diff --git a/internal_packages/account-sidebar/lib/sidebar-item.coffee b/internal_packages/account-sidebar/lib/sidebar-item.coffee index da5e93f64..9f9842307 100644 --- a/internal_packages/account-sidebar/lib/sidebar-item.coffee +++ b/internal_packages/account-sidebar/lib/sidebar-item.coffee @@ -115,10 +115,13 @@ class SidebarItem id += "-#{opts.name}" if opts.name opts.name = "Snoozed" unless opts.name opts.iconName= 'snooze.png' + categories = accountIds.map (accId) => - _.findWhere CategoryStore.userCategories(accId), {displayName} + _.findWhere CategoryStore.categories(accId), {displayName} + categories = _.compact(categories) perspective = MailboxPerspective.forCategories(categories) + perspective.name = id unless perspective.name @forPerspective(id, perspective, opts) @forStarred: (accountIds, opts = {}) -> diff --git a/internal_packages/composer-emojis/stylesheets/composer-emojis.less b/internal_packages/composer-emojis/stylesheets/composer-emojis.less index 146a103ec..124d967a2 100644 --- a/internal_packages/composer-emojis/stylesheets/composer-emojis.less +++ b/internal_packages/composer-emojis/stylesheets/composer-emojis.less @@ -6,7 +6,7 @@ overflow: auto; .btn.btn-icon { font-size: 14px !important; - padding: 0em 0.5em; + padding: 0 0.5em; &:first-child { padding-left: 0.5em !important; } diff --git a/internal_packages/composer-templates/lib/template-picker.jsx b/internal_packages/composer-templates/lib/template-picker.jsx index e92ebe744..deff81100 100644 --- a/internal_packages/composer-templates/lib/template-picker.jsx +++ b/internal_packages/composer-templates/lib/template-picker.jsx @@ -64,7 +64,7 @@ class TemplatePicker extends React.Component { render() { const button = ( - + ) } } diff --git a/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 b/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 index 687782a67..5aa933275 100644 --- a/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 +++ b/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 @@ -29,7 +29,8 @@ export default class LinkTrackingComposerExtension extends ComposerExtension { // loop through all elements, replace with redirect links and save mappings draftBody.unquoted = draftBody.unquoted.replace(RegExpUtils.linkTagRegex(), (match, prefix, url, suffix, content, closingTag) => { const encoded = encodeURIComponent(url); - const redirectUrl = `http://${PLUGIN_URL}/${draft.accountId}/${messageUid}/${links.length}?redirect=${encoded}`; + // the links param is an index of the link array. + const redirectUrl = `${PLUGIN_URL}/link/${draft.accountId}/${messageUid}/${links.length}?redirect=${encoded}`; links.push({url: url, click_count: 0, click_data: [], redirect_url: redirectUrl}); return prefix + redirectUrl + suffix + content + closingTag; }); diff --git a/internal_packages/link-tracking/lib/link-tracking-constants.es6 b/internal_packages/link-tracking/lib/link-tracking-constants.es6 index a1d78b44e..963fb2368 100644 --- a/internal_packages/link-tracking/lib/link-tracking-constants.es6 +++ b/internal_packages/link-tracking/lib/link-tracking-constants.es6 @@ -2,4 +2,4 @@ import plugin from '../package.json' export const PLUGIN_NAME = plugin.title export const PLUGIN_ID = plugin.appId[NylasEnv.config.get("env")]; -export const PLUGIN_URL = "https://edgehill-staging.nylas.com/plugins"; +export const PLUGIN_URL = plugin.serverUrl[NylasEnv.config.get("env")]; diff --git a/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 b/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 index 04d343295..d163def20 100644 --- a/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 +++ b/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 @@ -1,9 +1,9 @@ import {MessageViewExtension, RegExpUtils} from 'nylas-exports' -import plugin from '../package.json' +import {PLUGIN_ID} from './link-tracking-constants' export default class LinkTrackingMessageExtension extends MessageViewExtension { static formatMessageBody({message}) { - const metadata = message.metadataForPluginId(plugin.appId) || {}; + const metadata = message.metadataForPluginId(PLUGIN_ID) || {}; if ((metadata.links || []).length === 0) { return } const links = {} for (const link of metadata.links) { diff --git a/internal_packages/link-tracking/lib/main.es6 b/internal_packages/link-tracking/lib/main.es6 index 81298bb3f..43d1ae0b0 100644 --- a/internal_packages/link-tracking/lib/main.es6 +++ b/internal_packages/link-tracking/lib/main.es6 @@ -25,13 +25,13 @@ function afterDraftSend({draftClientId}) { // post the uid and message id pair to the plugin server const data = {uid: uid, message_id: message.id}; - const serverUrl = `${PLUGIN_URL}/register-message`; + const serverUrl = `${PLUGIN_URL}/plugins/register-message`; return post({ url: serverUrl, body: JSON.stringify(data), }).then( ([response, responseBody]) => { if (response.statusCode !== 200) { - throw new Error(); + throw new Error(`Link Tracking server error ${response.statusCode} at ${serverUrl}: ${responseBody}`); } return responseBody; }).catch(error => { diff --git a/internal_packages/link-tracking/package.json b/internal_packages/link-tracking/package.json index 3ed8ca93c..8e5d577c2 100644 --- a/internal_packages/link-tracking/package.json +++ b/internal_packages/link-tracking/package.json @@ -6,6 +6,10 @@ "staging": "2lsb6ounfysvwcdzxzy3bzsuh", "production": "a1ec1s3ieddpik6lpob74hmcq" }, + "serverUrl": { + "staging": "https://link-staging.nylas.com", + "production": "https://link.nylas.com" + }, "title": "Link Tracking", "description": "Tracks whether links in an email have been clicked by recipients", diff --git a/internal_packages/message-list/lib/email-frame.cjsx b/internal_packages/message-list/lib/email-frame.cjsx index d7918d9f1..b116fd9f9 100644 --- a/internal_packages/message-list/lib/email-frame.cjsx +++ b/internal_packages/message-list/lib/email-frame.cjsx @@ -1,7 +1,7 @@ React = require 'react' _ = require "underscore" {EventedIFrame} = require 'nylas-component-kit' -{QuotedHTMLTransformer} = require 'nylas-exports' +{Utils, QuotedHTMLTransformer} = require 'nylas-exports' EmailFrameStylesStore = require './email-frame-styles-store' @@ -11,9 +11,6 @@ class EmailFrame extends React.Component @propTypes: content: React.PropTypes.string.isRequired - constructor: (@props) -> - @_lastComputedHeight = 0 - render: => @@ -29,14 +26,12 @@ class EmailFrame extends React.Component componentDidUpdate: => @_writeContent() - shouldComponentUpdate: (newProps, newState) => - # Turns out, React is not able to tell if props.children has changed, - # so whenever the message list updates each email-frame is repopulated, - # often with the exact same content. To avoid unnecessary calls to - # _writeContent, we do a quick check for deep equality. - !_.isEqual(newProps, @props) + shouldComponentUpdate: (nextProps, nextState) => + not Utils.isEqualReact(nextProps, @props) or + not Utils.isEqualReact(nextState, @state) _writeContent: => + @_lastComputedHeight = 0 domNode = React.findDOMNode(@) doc = domNode.contentDocument return unless doc diff --git a/internal_packages/message-list/lib/message-item-body.cjsx b/internal_packages/message-list/lib/message-item-body.cjsx index b3c0fa8cb..d911c2d43 100644 --- a/internal_packages/message-list/lib/message-item-body.cjsx +++ b/internal_packages/message-list/lib/message-item-body.cjsx @@ -22,11 +22,20 @@ class MessageItemBody extends React.Component processedBody: undefined componentWillMount: => - @_unsub = MessageBodyProcessor.processAndSubscribe(@props.message, @_onBodyChanged) + @_prepareBody(@props) - componentWillReceiveProps: (newProps) => + shouldComponentUpdate: (nextProps, nextState) -> + not Utils.isEqualReact(nextProps, @props) or + not Utils.isEqualReact(nextState, @state) + + componentWillUpdate: (nextProps, nextState) => + if not Utils.isEqualReact(nextProps.message, @props.message) + @_prepareBody(nextProps) + + _prepareBody: (props) -> + MessageBodyProcessor.resetCache(props.message) @_unsub?() - @_unsub = MessageBodyProcessor.processAndSubscribe(newProps.message, @_onBodyChanged) + @_unsub = MessageBodyProcessor.processAndSubscribe(props.message, @_onBodyChanged) componentWillUnmount: => @_unsub?() diff --git a/internal_packages/message-list/lib/plugins/tracking-pixels-extension.coffee b/internal_packages/message-list/lib/plugins/tracking-pixels-extension.coffee index 1e6ee6486..44e2faf64 100644 --- a/internal_packages/message-list/lib/plugins/tracking-pixels-extension.coffee +++ b/internal_packages/message-list/lib/plugins/tracking-pixels-extension.coffee @@ -96,6 +96,10 @@ TrackingBlacklist = [{ name: 'Salesloft', pattern: 'sdr.salesloft.com/email_trackers', homepage: 'http://salesloft.com' + }, { + name: 'Nylas', + pattern: 'nylas.com/open', + homepage: 'http://nylas.com/N1' }] class TrackingPixelsExtension extends MessageViewExtension diff --git a/internal_packages/message-list/spec/message-item-body-spec.cjsx b/internal_packages/message-list/spec/message-item-body-spec.cjsx index 725903724..c063af545 100644 --- a/internal_packages/message-list/spec/message-item-body-spec.cjsx +++ b/internal_packages/message-list/spec/message-item-body-spec.cjsx @@ -106,7 +106,7 @@ describe "MessageItem", -> snippet: "snippet one..." subject: "Subject One" threadId: "thread_12345" - accountId: TEST_ACCOUNT_ID + accountId: window.TEST_ACCOUNT_ID # Generate the test component. Should be called after @message is configured # for the test, since MessageItem assumes attributes of the message will not diff --git a/internal_packages/open-tracking/lib/main.es6 b/internal_packages/open-tracking/lib/main.es6 index 569400632..0a3fcd418 100644 --- a/internal_packages/open-tracking/lib/main.es6 +++ b/internal_packages/open-tracking/lib/main.es6 @@ -27,7 +27,7 @@ function afterDraftSend({draftClientId}) { // post the uid and message id pair to the plugin server const data = {uid: uid, message_id: message.id, thread_id: 1}; - const serverUrl = `${PLUGIN_URL}/register-message`; + const serverUrl = `${PLUGIN_URL}/plugins/register-message`; return post({ url: serverUrl, body: JSON.stringify(data), diff --git a/internal_packages/open-tracking/lib/open-tracking-button.jsx b/internal_packages/open-tracking/lib/open-tracking-button.jsx index b379c5707..31c2e30c7 100644 --- a/internal_packages/open-tracking/lib/open-tracking-button.jsx +++ b/internal_packages/open-tracking/lib/open-tracking-button.jsx @@ -1,61 +1,38 @@ -import {DraftStore, React, Actions, NylasAPI, DatabaseStore, Message, Rx} from 'nylas-exports' -import {RetinaImg} from 'nylas-component-kit' +// import {DraftStore, React, Actions, NylasAPI, DatabaseStore, Message, Rx} from 'nylas-exports' +import {React, APIError, NylasAPI} from 'nylas-exports' +import {MetadataComposerToggleButton} from 'nylas-component-kit' import {PLUGIN_ID, PLUGIN_NAME} from './open-tracking-constants' - export default class OpenTrackingButton extends React.Component { - static displayName = 'OpenTrackingButton'; static propTypes = { draftClientId: React.PropTypes.string.isRequired, }; - constructor(props) { - super(props); - this.state = {enabled: false}; + _title(enabled) { + const dir = enabled ? "Disable" : "Enable"; + return `${dir} read receipts` } - componentDidMount() { - const query = DatabaseStore.findBy(Message, {clientId: this.props.draftClientId}); - this._subscription = Rx.Observable.fromQuery(query).subscribe(this.setStateFromDraft) + _errorMessage(error) { + if (error instanceof APIError && error.statusCode === NylasAPI.TimeoutErrorCode) { + return `Read receipts do not work offline. Please re-enable when you come back online.` + } + return `Unfortunately, read receipts are currently not available. Please try again later.` } - componentWillUnmount() { - this._subscription.dispose(); - } - - setStateFromDraft = (draft)=> { - if (!draft) return; - const metadata = draft.metadataForPluginId(PLUGIN_ID); - this.setState({enabled: metadata ? metadata.tracked : false}); - }; - - _onClick=()=> { - const currentlyEnabled = this.state.enabled; - - // write metadata into the draft to indicate tracked state - DraftStore.sessionForClientId(this.props.draftClientId).then((session)=> { - const draft = session.draft(); - - NylasAPI.authPlugin(PLUGIN_ID, PLUGIN_NAME, draft.accountId) - .then(() => { - Actions.setMetadata(draft, PLUGIN_ID, currentlyEnabled ? null : {tracked: true}); - }) - .catch((error)=> { - NylasEnv.reportError(error); - NylasEnv.showErrorDialog(`Sorry, we were unable to save your read receipt settings. ${error.message}`); - }); - }); - }; - render() { - const title = this.state.enabled ? "Disable" : "Enable"; - return () + return ( + + ) } - } diff --git a/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 b/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 index c25f5f5e6..d99ca9269 100644 --- a/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 +++ b/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 @@ -21,7 +21,7 @@ export default class OpenTrackingComposerExtension extends ComposerExtension { const uid = uuid.v4().replace(/-/g, ""); // insert a tracking pixel into the message - const serverUrl = `http://${PLUGIN_URL}/${draft.accountId}/${uid}`; + const serverUrl = `${PLUGIN_URL}/open/${draft.accountId}/${uid}`; const img = ``; const draftBody = new DraftBody(draft); draftBody.unquoted = draftBody.unquoted + "
" + img; diff --git a/internal_packages/open-tracking/lib/open-tracking-constants.es6 b/internal_packages/open-tracking/lib/open-tracking-constants.es6 index a1d78b44e..963fb2368 100644 --- a/internal_packages/open-tracking/lib/open-tracking-constants.es6 +++ b/internal_packages/open-tracking/lib/open-tracking-constants.es6 @@ -2,4 +2,4 @@ import plugin from '../package.json' export const PLUGIN_NAME = plugin.title export const PLUGIN_ID = plugin.appId[NylasEnv.config.get("env")]; -export const PLUGIN_URL = "https://edgehill-staging.nylas.com/plugins"; +export const PLUGIN_URL = plugin.serverUrl[NylasEnv.config.get("env")]; diff --git a/internal_packages/open-tracking/lib/open-tracking-icon.jsx b/internal_packages/open-tracking/lib/open-tracking-icon.jsx index 578bb7061..d16f6e265 100644 --- a/internal_packages/open-tracking/lib/open-tracking-icon.jsx +++ b/internal_packages/open-tracking/lib/open-tracking-icon.jsx @@ -39,11 +39,10 @@ export default class OpenTrackingIcon extends React.Component { } render() { - if (!this.state.hasMetadata) { return false } const title = this.state.opened ? "This message has been read at least once" : "This message has not been read"; return (
- {this._renderImage()} + {this.state.hasMetadata ? this._renderImage() : ""}
); } diff --git a/internal_packages/open-tracking/lib/open-tracking-message-status.jsx b/internal_packages/open-tracking/lib/open-tracking-message-status.jsx index 4ac755ca3..556e57a2f 100644 --- a/internal_packages/open-tracking/lib/open-tracking-message-status.jsx +++ b/internal_packages/open-tracking/lib/open-tracking-message-status.jsx @@ -1,6 +1,6 @@ import {React} from 'nylas-exports' import {RetinaImg} from 'nylas-component-kit' -import plugin from '../package.json' +import {PLUGIN_ID} from './open-tracking-constants' export default class OpenTrackingMessageStatus extends React.Component { static displayName = "OpenTrackingMessageStatus"; @@ -14,8 +14,12 @@ export default class OpenTrackingMessageStatus extends React.Component { this.state = this._getStateFromMessage(props.message) } + componentWillReceiveProps(nextProps) { + this.setState(this._getStateFromMessage(nextProps.message)) + } + _getStateFromMessage(message) { - const metadata = message.metadataForPluginId(plugin.appId); + const metadata = message.metadataForPluginId(PLUGIN_ID); if (!metadata) { return {hasMetadata: false, opened: false} } diff --git a/internal_packages/open-tracking/package.json b/internal_packages/open-tracking/package.json index 26ad9e2ed..f1c2717cf 100644 --- a/internal_packages/open-tracking/package.json +++ b/internal_packages/open-tracking/package.json @@ -6,6 +6,10 @@ "staging": "3i2bws091xrj7fpb1aqt2tud6", "production": "1hnytbkg4wd1ahodatwxdqlb5" }, + "serverUrl": { + "staging": "https://link-staging.nylas.com", + "production": "https://link.nylas.com" + }, "title": "Open Tracking", "description": "Tracks whether email messages have been opened by recipients", diff --git a/internal_packages/open-tracking/stylesheets/main.less b/internal_packages/open-tracking/stylesheets/main.less index 051bea281..e3a395492 100644 --- a/internal_packages/open-tracking/stylesheets/main.less +++ b/internal_packages/open-tracking/stylesheets/main.less @@ -1,13 +1,26 @@ @import "ui-variables"; @import "ui-mixins"; +.open-tracking-icon img { + vertical-align: initial; +} + .open-tracking-icon img.content-mask.unopened { - background-color: #6b777d; - vertical-align: text-bottom; + background-color: fadeout(@text-color, 80%); } .open-tracking-icon img.content-mask.opened { background-color: @text-color-link; } + +.list-item.focused { + .open-tracking-icon img.content-mask.unopened { + background-color: fadeout(@text-color-inverse, 70%); + } + .open-tracking-icon img.content-mask.opened { + background-color: @background-off-primary; + } +} + .open-tracking-icon .open-count { display: inline-block; position: relative; @@ -18,9 +31,10 @@ font-size: 12px; font-weight: bold; } + .open-tracking-icon { - height: 10px; - margin: -9px 0 0 4px; + width: 15px; + margin: 0 2px; } .read-receipt-message-status { diff --git a/internal_packages/quick-schedule/lib/calendar-button.cjsx b/internal_packages/quick-schedule/lib/calendar-button.cjsx index 706ef61ed..c7fc2e94e 100644 --- a/internal_packages/quick-schedule/lib/calendar-button.cjsx +++ b/internal_packages/quick-schedule/lib/calendar-button.cjsx @@ -5,7 +5,7 @@ class CalendarButton extends React.Component @displayName: 'CalendarButton' render: => - diff --git a/internal_packages/send-later/lib/send-later-popover.jsx b/internal_packages/send-later/lib/send-later-popover.jsx index 83617e42a..52024cb70 100644 --- a/internal_packages/send-later/lib/send-later-popover.jsx +++ b/internal_packages/send-later/lib/send-later-popover.jsx @@ -1,5 +1,4 @@ /** @babel */ -import _ from 'underscore' import Rx from 'rx-lite' import React, {Component, PropTypes} from 'react' import {DateUtils, Message, DatabaseStore} from 'nylas-exports' @@ -23,7 +22,7 @@ class SendLaterPopover extends Component { static displayName = 'SendLaterPopover'; static propTypes = { - draftClientId: PropTypes.string, + draftClientId: PropTypes.string.isRequired, }; constructor(props) { @@ -38,9 +37,15 @@ class SendLaterPopover extends Component { this._subscription = Rx.Observable.fromQuery( DatabaseStore.findBy(Message, {clientId: this.props.draftClientId}) ).subscribe((draft)=> { - const scheduledDate = SendLaterStore.getScheduledDateForMessage(draft); - if (scheduledDate !== this.state.scheduledDate) { - this.setState({scheduledDate}); + const nextScheduledDate = SendLaterStore.getScheduledDateForMessage(draft); + + if (nextScheduledDate !== this.state.scheduledDate) { + const isPopout = (NylasEnv.getWindowType() === "composer"); + const isFinishedSelecting = ((this.state.scheduledDate === 'saving') && (nextScheduledDate !== null)); + if (isPopout && isFinishedSelecting) { + NylasEnv.close(); + } + this.setState({scheduledDate: nextScheduledDate}); } }); } @@ -51,29 +56,51 @@ class SendLaterPopover extends Component { onSelectMenuOption = (optionKey)=> { const date = SendLaterOptions[optionKey](); - const formatted = DateUtils.format(date.utc()) - - SendLaterActions.sendLater(this.props.draftClientId, formatted) - this.setState({scheduledDate: 'saving', inputDate: null}) - this.refs.popover.close() + this.onSelectDate(date); }; + onSelectCustomOption = (value)=> { + const date = DateUtils.futureDateFromString(value); + if (date) { + this.onSelectDate(date); + } else { + NylasEnv.showErrorDialog(`Sorry, we can't parse ${value} as a valid date.`); + } + } + + onSelectDate = (date)=> { + const formatted = DateUtils.format(date.utc()); + SendLaterActions.sendLater(this.props.draftClientId, formatted); + this.setState({scheduledDate: 'saving', inputDate: null}); + this.refs.popover.close(); + } + onCancelSendLater = ()=> { - SendLaterActions.cancelSendLater(this.props.draftClientId) - this.setState({inputDate: null}) - this.refs.popover.close() + SendLaterActions.cancelSendLater(this.props.draftClientId); + this.setState({inputDate: null}); + this.refs.popover.close(); }; renderCustomTimeSection() { - const updateInputDateValue = _.debounce((value)=> { - this.setState({inputDate: DateUtils.fromString(value)}) - }, 250); + const onChange = (event)=> { + this.setState({inputDate: DateUtils.futureDateFromString(event.target.value)}); + } + + const onKeyDown = (event)=> { + // we need to swallow these events so they don't reach the menu + // containing the text input, but only when you've typed something. + const val = event.target.value; + if ((val.length > 0) && ((event.keyCode === 13) || (event.keyCode === 39))) { + this.onSelectCustomOption(val); + event.stopPropagation(); + } + }; let dateInterpretation = false; if (this.state.inputDate) { - dateInterpretation = ( + dateInterpretation = ( {DateUtils.format(this.state.inputDate, DATE_FORMAT_LONG)} - ); + ); } return ( @@ -81,8 +108,9 @@ class SendLaterPopover extends Component { updateInputDateValue(event.target.value)}/> + placeholder="Or, 'next Monday at 2PM'" + onKeyDown={onKeyDown} + onChange={onChange}/> {dateInterpretation} ) @@ -92,7 +120,7 @@ class SendLaterPopover extends Component { const date = SendLaterOptions[optionKey](); const formatted = DateUtils.format(date, DATE_FORMAT_SHORT); return ( -
{optionKey}{formatted}
+
{optionKey}{formatted}
); } @@ -102,7 +130,7 @@ class SendLaterPopover extends Component { if (scheduledDate === 'saving') { return ( - + ); + } + +} diff --git a/src/components/swipe-container.jsx b/src/components/swipe-container.jsx index 3e24d45ee..b6357117e 100644 --- a/src/components/swipe-container.jsx +++ b/src/components/swipe-container.jsx @@ -1,4 +1,5 @@ import React, {Component, PropTypes} from 'react'; +import DOMUtils from '../dom-utils'; import _ from 'underscore'; import {exec} from 'child_process'; @@ -59,6 +60,7 @@ export default class SwipeContainer extends Component { onSwipeLeftClass: React.PropTypes.string, onSwipeRight: React.PropTypes.func, onSwipeRightClass: React.PropTypes.string, + onSwipeCenter: React.PropTypes.func, }; constructor(props) { @@ -155,7 +157,7 @@ export default class SwipeContainer extends Component { _onScrollTouchEnd = ()=> { this.tracking = false; - if (this.phase !== Phase.None) { + if ((this.phase !== Phase.None) && (this.phase !== Phase.Settling)) { this.phase = Phase.Settling; this.fired = false; this.setState({ @@ -244,15 +246,16 @@ export default class SwipeContainer extends Component { const shouldFinish = (f >= 1.0); const mostlyFinished = ((Math.abs(currentX) / Math.abs(targetX)) > 0.8); - const shouldFire = (targetX !== 0) && mostlyFinished && (this.fired === false); + const shouldFire = mostlyFinished && (this.fired === false); if (shouldFire) { this.fired = true; - if (targetX > 0) { + if ((targetX > 0) && this.props.onSwipeRight) { this.props.onSwipeRight(this._onSwipeActionCompleted); - } - if (targetX < 0) { + } else if ((targetX < 0) && this.props.onSwipeLeft) { this.props.onSwipeLeft(this._onSwipeActionCompleted); + } else if ((targetX == 0) && this.props.onSwipeCenter) { + this.props.onSwipeCenter(); } } diff --git a/src/date-utils.es6 b/src/date-utils.es6 index 3034060cc..caa37200e 100644 --- a/src/date-utils.es6 +++ b/src/date-utils.es6 @@ -1,6 +1,51 @@ /** @babel */ import moment from 'moment' import chrono from 'chrono-node' +import _ from 'underscore' + +function isPastDate({year, month, day}, ref) { + const refDay = ref.getDate(); + const refMonth = ref.getMonth() + 1; + const refYear = ref.getFullYear(); + if (refYear > year) { + return true; + } + if (refMonth > month) { + return true; + } + if (refDay > day) { + return true; + } + return false; +} + +const EnforceFutureDate = new chrono.Refiner(); +EnforceFutureDate.refine = (text, results)=> { + results.forEach((result)=> { + const current = _.extend({}, result.start.knownValues, result.start.impliedValues); + + if (result.start.isCertain('weekday') && !result.start.isCertain('day')) { + if (isPastDate(current, result.ref)) { + result.start.imply('day', result.start.impliedValues.day + 7); + } + } + + if (result.start.isCertain('day') && !result.start.isCertain('month')) { + if (isPastDate(current, result.ref)) { + result.start.imply('month', result.start.impliedValues.month + 1); + } + } + if (result.start.isCertain('month') && !result.start.isCertain('year')) { + if (isPastDate(current, result.ref)) { + result.start.imply('year', result.start.impliedValues.year + 1); + } + } + }); + return results; +}; + +const chronoFuture = new chrono.Chrono(chrono.options.casualOption()); +chronoFuture.refiners.push(EnforceFutureDate); const Hours = { Morning: 9, @@ -82,12 +127,12 @@ const DateUtils = { /** * Can take almost any string. - * e.g. "Next monday at 2pm" + * e.g. "Next Monday at 2pm" * @param {string} dateLikeString - a string representing a date. * @return {moment} - moment object representing date */ - fromString(dateLikeString) { - const date = chrono.parseDate(dateLikeString) + futureDateFromString(dateLikeString) { + const date = chronoFuture.parseDate(dateLikeString) if (!date) { return null } diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index a07d7084e..58edbd3dd 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -126,8 +126,7 @@ class Actions @longPollOffline: ActionScopeWorkWindow @willMakeAPIRequest: ActionScopeWorkWindow @didMakeAPIRequest: ActionScopeWorkWindow - @sendFeedback: ActionScopeWindow - + ### Public: Retry the initial sync diff --git a/src/flux/models/category.coffee b/src/flux/models/category.coffee index eba041584..a85c818a4 100644 --- a/src/flux/models/category.coffee +++ b/src/flux/models/category.coffee @@ -18,6 +18,7 @@ StandardCategories = { LockedCategories = { "sent" + "N1-Snoozed" } HiddenCategories = { @@ -27,6 +28,7 @@ HiddenCategories = { "archive" "starred" "important" + "N1-Snoozed" } AllMailName = "all" @@ -110,10 +112,10 @@ class Category extends Model StandardCategories[@name]? and @name isnt 'important' isLockedCategory: -> - LockedCategories[@name]? + LockedCategories[@name]? or LockedCategories[@displayName]? isHiddenCategory: -> - HiddenCategories[@name]? + HiddenCategories[@name]? or HiddenCategories[@displayName]? isUserCategory: -> not @isStandardCategory() and not @isHiddenCategory() diff --git a/src/flux/stores/message-body-processor.coffee b/src/flux/stores/message-body-processor.coffee index ccae31f08..2bacbbc96 100644 --- a/src/flux/stores/message-body-processor.coffee +++ b/src/flux/stores/message-body-processor.coffee @@ -11,13 +11,17 @@ class MessageBodyProcessor @_subscriptions = [] @resetCache() - resetCache: -> - # Store an object for recently processed items. Put the item reference into - # both data structures so we can access it in O(1) and also delete in O(1) - @_recentlyProcessedA = [] - @_recentlyProcessedD = {} - for {message, callback} in @_subscriptions - callback(@process(message)) + resetCache: (msg) -> + if msg + key = @_key(msg) + delete @_recentlyProcessedD[key] + else + # Store an object for recently processed items. Put the item reference into + # both data structures so we can access it in O(1) and also delete in O(1) + @_recentlyProcessedA = [] + @_recentlyProcessedD = {} + for {message, callback} in @_subscriptions + callback(@process(message)) # It's far safer to key off the hash of the body then the [id, version] # pair. This is because it's theoretically possible for the body to diff --git a/src/flux/stores/popover-store.jsx b/src/flux/stores/popover-store.jsx index 33f5d3f1f..992a5f18b 100644 --- a/src/flux/stores/popover-store.jsx +++ b/src/flux/stores/popover-store.jsx @@ -20,15 +20,23 @@ class PopoverStore extends NylasStore { this.container = createContainer(containerId); React.render(, this.container); - this.listenTo(Actions.openPopover, this.onOpenPopover); - this.listenTo(Actions.closePopover, this.onClosePopover); + this.listenTo(Actions.openPopover, this.openPopover); + this.listenTo(Actions.closePopover, this.closePopover); } isPopoverOpen = ()=> { return this.isOpen; }; - onOpenPopover = (element, originRect, direction)=> { + renderPopover = (popover, isOpen, callback)=> { + React.render(popover, this.container, ()=> { + this.isOpen = isOpen; + this.trigger(); + callback() + }) + }; + + openPopover = (element, originRect, direction, callback = ()=> {})=> { const popover = ( {element} - ) - React.render(popover, this.container, ()=> { - this.isOpen = true; - this.trigger(); - }) + ); + + if (this.isOpen) { + this.closePopover(()=> { + this.renderPopover(popover, true, callback); + }) + } else { + this.renderPopover(popover, true, callback); + } }; - onClosePopover = ()=> { + closePopover = (callback = ()=>{})=> { const popover = ; - React.render(popover, this.container, ()=> { - this.isOpen = false; - this.trigger(); - }) + this.renderPopover(popover, false, callback); }; } diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee index 52fd65552..4ef18a29f 100644 --- a/src/global/nylas-component-kit.coffee +++ b/src/global/nylas-component-kit.coffee @@ -32,6 +32,7 @@ class NylasComponentKit @load "MultiselectActionBar", 'multiselect-action-bar' @load "InjectedComponentSet", 'injected-component-set' @load "TimeoutTransitionGroup", 'timeout-transition-group' + @load "MetadataComposerToggleButton", 'metadata-composer-toggle-button' @load "ConfigPropContainer", "config-prop-container" @load "DisclosureTriangle", "disclosure-triangle" @load "EditableList", "editable-list" diff --git a/src/global/nylas-observables.coffee b/src/global/nylas-observables.coffee index dabe08c0d..ddf1a09b6 100644 --- a/src/global/nylas-observables.coffee +++ b/src/global/nylas-observables.coffee @@ -2,13 +2,8 @@ Rx = require 'rx-lite' _ = require 'underscore' Category = require '../flux/models/category' QuerySubscriptionPool = require '../flux/models/query-subscription-pool' -AccountStore = require '../flux/stores/account-store' DatabaseStore = require '../flux/stores/database-store' -AccountOperators = {} - -AccountObservables = {} - CategoryOperators = sort: -> obs = @.map (categories) -> @@ -60,10 +55,8 @@ CategoryObservables = CategoryObservables.forAccount(account).sort() .categoryFilter (cat) -> cat.isHiddenCategory() - module.exports = Categories: CategoryObservables - Accounts: AccountObservables # Attach a few global helpers diff --git a/src/nylas-env.coffee b/src/nylas-env.coffee index 5332416ed..771eef137 100644 --- a/src/nylas-env.coffee +++ b/src/nylas-env.coffee @@ -874,12 +874,21 @@ class NylasEnvConstructor extends Model dialog = remote.require('dialog') callback(dialog.showSaveDialog(@getCurrentWindow(), options)) - showErrorDialog: (message) -> + showErrorDialog: (messageData) -> + if _.isString(messageData) or _.isNumber(messageData) + message = messageData + title = "Error" + else if _.isObject(messageData) + message = messageData.message + title = messageData.title + else + throw new Error("Must pass a valid message to show dialog", message) + dialog = remote.require('dialog') dialog.showMessageBox null, { type: 'warning' buttons: ['Okay'], - message: "Error" + message: title detail: message } diff --git a/src/package.coffee b/src/package.coffee index 939d389b1..fff3cf4f9 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -75,7 +75,17 @@ class Package @metadata ?= Package.loadMetadata(@path) @bundledPackage = Package.isBundledPackagePath(@path) @name = @metadata?.name ? path.basename(@path) - @pluginAppId = @metadata.appId ? null + + if @metadata.appId + if _.isString @metadata.appId + @pluginAppId = @metadata.appId ? null + else if _.isObject @metadata.appId + @pluginAppId = @metadata.appId[NylasEnv.config.get('env')] ? null + else + @pluginAppId = null + else + @pluginAppId = null + @displayName = @metadata?.displayName || @name ModuleCache.add(@path, @metadata) @reset() diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 1d1726030..f06240e0e 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -30,10 +30,6 @@ class WindowEventHandler @subscribe ipcRenderer, 'update-available', (event, detail) -> NylasEnv.updateAvailable(detail) - @subscribe ipcRenderer, 'send-feedback', (detail) -> - Actions = require './flux/actions' - Actions.sendFeedback() - @subscribe ipcRenderer, 'browser-window-focus', -> document.body.classList.remove('is-blurred') diff --git a/static/components/contenteditable.less b/static/components/contenteditable.less index 36e5ea881..f54a9cfe9 100644 --- a/static/components/contenteditable.less +++ b/static/components/contenteditable.less @@ -42,24 +42,35 @@ margin-top: 0; } - .toolbar-pointer { + .toolbar-pointer-container { position: absolute; - width: 22.5px; + width: 23px; height: 10px; - background: transparent url('images/tooltip/tooltip-bg-pointer@2x.png') no-repeat; - background-size: 22.5px 9.5px; - margin-left: -11.2px; + + div { + -webkit-mask-image: url('images/tooltip/tooltip-bg-pointer@2x.png'); + background-color: #fff; + position: absolute; + zoom: 0.5; + width: 45px; + height: 21px; + &.shadow { + -webkit-mask-image: url('images/tooltip/tooltip-bg-pointer-shadow@2x.png'); + background-color: fade(@black, 22%); + transform: translateY(0.5px); + } + } } &.above { - .toolbar-pointer { - transform: rotate(0deg); + .toolbar-pointer-container { + transform: translateX(-11px) rotate(0deg); bottom: -9px; } } &.below { - .toolbar-pointer { - transform: rotate(180deg); + .toolbar-pointer-container { + transform: translateX(-11px) rotate(180deg); top: -9px; } } diff --git a/static/components/fixed-popover.less b/static/components/fixed-popover.less index 5ad47ad4c..009d0e2e0 100644 --- a/static/components/fixed-popover.less +++ b/static/components/fixed-popover.less @@ -14,6 +14,23 @@ background-color: @background-primary; border-radius: @border-radius-base; box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15); + + input[type=text] { + border: 1px solid darken(@background-secondary, 10%); + border-radius: 3px; + background-color: @background-primary; + box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05); + color: @text-color; + + &.search { + padding-left: 0; + background-repeat: no-repeat; + background-image: url("../static/images/search/searchloupe@2x.png"); + background-size: 15px 15px; + background-position: 7px 4px; + text-indent: 31px; + } + } } .fixed-popover-pointer,.fixed-popover-pointer.shadow { diff --git a/static/components/outline-view.less b/static/components/outline-view.less index e496b50a9..3c9f472bf 100644 --- a/static/components/outline-view.less +++ b/static/components/outline-view.less @@ -38,6 +38,7 @@ margin-right: @padding-small-horizontal; cursor: pointer; img {background: @text-color-very-subtle; } + display: none; } .collapse-button { @@ -49,7 +50,7 @@ } &:hover { - .collapse-button { + .collapse-button,.add-item-button { display: inherit; } }