Merge branch 'master' of github.com:nylas/N1

This commit is contained in:
Jackie Luo 2016-02-24 17:06:28 -08:00
commit 82e674292a
62 changed files with 566 additions and 364 deletions

View file

@ -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 = {}) ->

View file

@ -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;
}

View file

@ -64,7 +64,7 @@ class TemplatePicker extends React.Component {
render() {
const button = (
<button className="btn btn-toolbar narrow" title="Insert email template...">
<button className="btn btn-toolbar narrow" title="Insert email template">
<RetinaImg url="nylas://composer-templates/assets/icon-composer-templates@2x.png" mode={RetinaImg.Mode.ContentIsMask}/>
&nbsp;
<RetinaImg name="icon-composer-dropdown.png" mode={RetinaImg.Mode.ContentIsMask}/>

View file

@ -61,6 +61,7 @@ class TranslateButton extends React.Component
<Menu items={ Object.keys(YandexLanguages) }
itemKey={ (item) -> item }
itemContent={ (item) -> item }
defaultSelectedIndex={-1}
onSelect={@_onTranslate}
/>
</Popover>
@ -70,7 +71,7 @@ class TranslateButton extends React.Component
# `RetinaImg` will automatically chose the best image format for our display.
#
_renderButton: =>
<button className="btn btn-toolbar" title="Translate email body...">
<button className="btn btn-toolbar" title="Translate email body">
<RetinaImg mode={RetinaImg.Mode.ContentIsMask} url="nylas://composer-translate/assets/icon-composer-translate@2x.png" />
&nbsp;
<RetinaImg name="icon-composer-dropdown.png" mode={RetinaImg.Mode.ContentIsMask}/>

View file

@ -111,7 +111,7 @@ class ExpandedParticipants extends React.Component
{ if @props.mode is "inline"
<span className="header-action show-popout"
title="Popout composer"
title="Popout composer"
style={paddingLeft: "1.5em"}
onClick={@props.onPopoutComposer}>
<RetinaImg name="composer-popout.png"

View file

@ -1,29 +0,0 @@
{Utils,
React,
FocusedContactsStore,
AccountStore,
Actions} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
class FeedbackButton extends React.Component
@displayName: 'FeedbackButton'
constructor: (@props) ->
componentDidMount: =>
@_unsubs = []
@_unsubs.push Actions.sendFeedback.listen(@_onSendFeedback)
componentWillUnmount: =>
unsub() for unsub in @_unsubs
render: =>
<div style={position:"absolute",height:0} title="Help & Feedback">
<div className="btn-feedback" onClick={@_onSendFeedback}>?</div>
</div>
_onSendFeedback: =>
return if NylasEnv.inSpecMode()
require('electron').shell.openExternal('https://nylas.zendesk.com/hc/en-us/sections/203638587-N1')
module.exports = FeedbackButton

View file

@ -1,13 +0,0 @@
{WorkspaceStore, ComponentRegistry} = require 'nylas-exports'
FeedbackButton = require './feedback-button'
protocol = require('remote').require('protocol')
module.exports =
activate: (@state) ->
ComponentRegistry.register FeedbackButton,
location: WorkspaceStore.Sheet.Global.Footer
serialize: ->
deactivate: ->
ComponentRegistry.unregister(FeedbackButton)

View file

@ -1,12 +0,0 @@
{
"name": "feedback",
"main": "./lib/main",
"version": "0.1.0",
"engines": {
"nylas": "*"
},
"description": "Intercom feeedback",
"dependencies": {
},
"private":true
}

View file

@ -1,27 +0,0 @@
@import "ui-variables";
@import "ui-mixins";
.btn-feedback {
position: fixed;
bottom: 7px;
right: 7px;
background: linear-gradient(to bottom, #419bf9 0%, #1081f7 100%);
width: 38px;
height: 38px;
border-radius: 25px;
display: inline-block;
font-size: 28px;
text-align: center;
line-height: 37px;
color: rgba(255, 255, 255, 0.9);
border: 1px solid #0668ce;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
cursor: default;
}
.btn-feedback:hover {
color:rgba(255,255,255,1);
background: linear-gradient(to bottom, lighten(@blue,5%) 0%, darken(@blue, 5%) 100%);
}
.btn-feedback:active {
background: linear-gradient(to bottom, darken(@blue,20%) 0%, darken(@blue, 10%) 100%);
}

View file

@ -1,8 +1,8 @@
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 './link-tracking-constants'
export default class LinkTrackingButton extends React.Component {
static displayName = 'LinkTrackingButton';
@ -10,55 +10,29 @@ export default class LinkTrackingButton extends React.Component {
draftClientId: React.PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.state = {enabled: false};
_title(enabled) {
const dir = enabled ? "Disable" : "Enable";
return `${dir} link tracking`
}
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 `Link tracking does not work offline. Please re-enable when you come back online.`
}
return `Unfortunately, link tracking servers 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 link tracking settings. ${error.message}`);
});
});
};
render() {
const title = this.state.enabled ? "Disable" : "Enable"
return (
<button
title={`${title} link tracking`}
className={`btn btn-toolbar ${this.state.enabled ? "btn-enabled" : ""}`}
onClick={this._onClick}>
<RetinaImg
name="icon-composer-linktracking.png"
mode={RetinaImg.Mode.ContentIsMask} />
</button>
<MetadataComposerToggleButton
title={this._title}
iconName="icon-composer-linktracking.png"
pluginId={PLUGIN_ID}
pluginName={PLUGIN_NAME}
metadataKey="tracked"
stickyToggle
errorMessage={this._errorMessage}
draftClientId={this.props.draftClientId} />
)
}
}

View file

@ -29,7 +29,8 @@ export default class LinkTrackingComposerExtension extends ComposerExtension {
// loop through all <a href> 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;
});

View file

@ -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")];

View file

@ -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) {

View file

@ -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 => {

View file

@ -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",

View file

@ -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: =>
<EventedIFrame ref="iframe" seamless="seamless" onResize={@_setFrameHeight}/>
@ -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

View file

@ -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?()

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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 (<button className={`btn btn-toolbar ${this.state.enabled ? "btn-enabled" : ""}`}
onClick={this._onClick} title={`${title} read receipts`}>
<RetinaImg url="nylas://open-tracking/assets/icon-composer-eye@2x.png"
mode={RetinaImg.Mode.ContentIsMask} />
</button>)
return (
<MetadataComposerToggleButton
title={this._title}
iconUrl="nylas://open-tracking/assets/icon-composer-eye@2x.png"
pluginId={PLUGIN_ID}
pluginName={PLUGIN_NAME}
metadataKey="tracked"
stickyToggle
errorMessage={this._errorMessage}
draftClientId={this.props.draftClientId} />
)
}
}

View file

@ -21,7 +21,7 @@ export default class OpenTrackingComposerExtension extends ComposerExtension {
const uid = uuid.v4().replace(/-/g, "");
// insert a tracking pixel <img> into the message
const serverUrl = `http://${PLUGIN_URL}/${draft.accountId}/${uid}`;
const serverUrl = `${PLUGIN_URL}/open/${draft.accountId}/${uid}`;
const img = `<img width="0" height="0" style="border:0; width:0; height:0;" src="${serverUrl}">`;
const draftBody = new DraftBody(draft);
draftBody.unquoted = draftBody.unquoted + "<br>" + img;

View file

@ -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")];

View file

@ -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 (
<div title={title} className="open-tracking-icon">
{this._renderImage()}
{this.state.hasMetadata ? this._renderImage() : ""}
</div>
);
}

View file

@ -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}
}

View file

@ -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",

View file

@ -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 {

View file

@ -5,7 +5,7 @@ class CalendarButton extends React.Component
@displayName: 'CalendarButton'
render: =>
<button className="btn btn-toolbar" onClick={@_onClick} title="Insert calendar availability...">
<button className="btn btn-toolbar" onClick={@_onClick} title="Insert calendar availability">
<RetinaImg url="nylas://quick-schedule/assets/icon-composer-quickschedule@2x.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>

View file

@ -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 = (<em>
dateInterpretation = (<span className="time">
{DateUtils.format(this.state.inputDate, DATE_FORMAT_LONG)}
</em>);
</span>);
}
return (
@ -81,8 +108,9 @@ class SendLaterPopover extends Component {
<input
tabIndex="1"
type="text"
placeholder="e.g. 'Sunday at 2PM'"
onChange={event=> updateInputDateValue(event.target.value)}/>
placeholder="Or, 'next Monday at 2PM'"
onKeyDown={onKeyDown}
onChange={onChange}/>
{dateInterpretation}
</div>
)
@ -92,7 +120,7 @@ class SendLaterPopover extends Component {
const date = SendLaterOptions[optionKey]();
const formatted = DateUtils.format(date, DATE_FORMAT_SHORT);
return (
<div className="send-later-option">{optionKey}<em>{formatted}</em></div>
<div className="send-later-option">{optionKey}<span className="time">{formatted}</span></div>
);
}
@ -102,7 +130,7 @@ class SendLaterPopover extends Component {
if (scheduledDate === 'saving') {
return (
<button className={className} title="Send later...">
<button className={className} title="Saving send date...">
<RetinaImg
name="inline-loading-spinner.gif"
mode={RetinaImg.Mode.ContentDark}
@ -114,13 +142,13 @@ class SendLaterPopover extends Component {
let dateInterpretation = false;
if (scheduledDate) {
className += ' btn-enabled';
const momentDate = DateUtils.fromString(scheduledDate);
const momentDate = DateUtils.futureDateFromString(scheduledDate);
if (momentDate) {
dateInterpretation = <span className="at">Sending in {momentDate.fromNow(true)}</span>;
}
}
return (
<button className={className}>
<button className={className} title="Send later">
<RetinaImg name="icon-composer-sendlater.png" mode={RetinaImg.Mode.ContentIsMask}/>
{dateInterpretation}
<span>&nbsp;</span>
@ -152,9 +180,11 @@ class SendLaterPopover extends Component {
style={{order: -103}}
className="send-later"
buttonComponent={this.renderButton()}>
<Menu items={ Object.keys(SendLaterOptions) }
<Menu ref="menu"
items={ Object.keys(SendLaterOptions) }
itemKey={ (item)=> item }
itemContent={this.renderMenuOption}
defaultSelectedIndex={-1}
footerComponents={footerComponents}
onSelect={this.onSelectMenuOption}
/>

View file

@ -24,9 +24,9 @@ export default class SendLaterStatus extends Component {
const formatted = DateUtils.format(moment(sendLaterDate), DATE_FORMAT_SHORT)
return (
<div className="send-later-status">
<em className="send-later-status">
<span className="time">
{`Scheduled for ${formatted}`}
</em>
</span>
<RetinaImg
name="image-cancel-button.png"
title="Cancel Send Later"

View file

@ -28,32 +28,30 @@ class SendLaterStore extends NylasStore {
};
setMetadata = (draftClientId, metadata)=> {
return (
DatabaseStore.modelify(Message, [draftClientId])
.then((messages)=> {
const {accountId} = messages[0];
return NylasAPI.authPlugin(this.pluginId, PLUGIN_NAME, accountId);
})
DatabaseStore.modelify(Message, [draftClientId]).then((messages)=> {
const {accountId} = messages[0];
NylasAPI.authPlugin(this.pluginId, PLUGIN_NAME, accountId)
.then(()=> {
Actions.setMetadata(messages, this.pluginId, metadata);
})
.catch((error)=> {
NylasEnv.reportError(error);
NylasEnv.showErrorDialog(`Sorry, we were unable to schedule this message. ${error.message}`);
})
);
});
});
};
onSendLater = (draftClientId, sendLaterDate)=> {
this.setMetadata(draftClientId, {sendLaterDate})
this.setMetadata(draftClientId, {sendLaterDate});
};
onCancelSendLater = (draftClientId)=> {
this.setMetadata(draftClientId, {sendLaterDate: null})
this.setMetadata(draftClientId, {sendLaterDate: null});
};
deactivate = ()=> {
this.unsubscribers.forEach(unsub => unsub())
this.unsubscribers.forEach(unsub => unsub());
};
}

View file

@ -2,8 +2,8 @@
.send-later {
em {
font-size: 0.9em;
.time {
font-size: @font-size-small;
opacity: 0.6;
}
@ -18,13 +18,14 @@
border-top-left-radius: @border-radius-base;
}
.item {
em {
.time {
display: none;
float: right;
padding-right: @padding-base-horizontal;
}
&.selected,
&:hover {
em {
.time {
display: inline-block;
}
}
@ -35,7 +36,7 @@
}
.custom-time-section {
padding: @padding-base-vertical @padding-base-horizontal;
em {
.time {
color: @text-color-subtle;
}
}
@ -59,9 +60,11 @@
display: flex;
align-items: center;
em {
.time {
font-size: 0.9em;
opacity: 0.62;
color: @component-active-color;
font-weight: @font-weight-normal;
}
img {
width: 38px;

View file

@ -125,30 +125,31 @@ class ThreadList extends React.Component
Actions.queueTasks(tasks)
callback(true)
props.onSwipeLeftClass = 'swipe-snooze'
props.onSwipeLeft = (callback) =>
# TODO this should be grabbed from elsewhere
{PopoverStore} = require 'nylas-exports'
SnoozePopoverBody = require '../../thread-snooze/lib/snooze-popover-body'
if perspective.isInbox()
props.onSwipeLeftClass = 'swipe-snooze'
props.onSwipeLeft = (callback) =>
# TODO this should be grabbed from elsewhere
{PopoverStore} = require 'nylas-exports'
SnoozePopoverBody = require '../../thread-snooze/lib/snooze-popover-body'
# TODO
# The question I want to ask is if I am already swiping, i.e. mid swipe,
# but I don't know how to ask it.
# This is good enough for now
if PopoverStore.isPopoverOpen()
callback(false)
return
# TODO
# The question I want to ask is if I am already swiping, i.e. mid swipe,
# but I don't know how to ask it.
# This is good enough for now
if PopoverStore.isPopoverOpen()
callback(false)
return
element = document.querySelector("[data-item-id=\"#{item.id}\"]")
rect = element.getBoundingClientRect()
Actions.openPopover(
<SnoozePopoverBody
threads={[item]}
swipeCallback={callback}
closePopover={Actions.closePopover}/>,
rect,
"right"
)
element = document.querySelector("[data-item-id=\"#{item.id}\"]")
rect = element.getBoundingClientRect()
Actions.openPopover(
<SnoozePopoverBody
threads={[item]}
swipeCallback={callback}
closePopover={Actions.closePopover}/>,
rect,
"right"
)
props

View file

@ -105,17 +105,19 @@
}
}
&.swipe-snooze {
transition: background-color linear 150ms;
background-color: mix(#8d6be3, @background-primary, 75%);
&::after {
transition: right linear 150ms, transform linear 150ms;
content: "Snooze";
left: 0;
right: 0;
background-image: url(../static/images/swipe/icon-swipe-snooze@2x.png);
}
&.confirmed {
background-color: #8d6be3;
&::after {
left: 100%;
transform: translateX(-100%);
right: 100%;
transform: translateX(100%);
opacity: 1;
}
}

View file

@ -1,5 +1,5 @@
import React, {Component, PropTypes} from 'react';
import {PopoverStore, Actions} from 'nylas-exports';
import {Actions} from 'nylas-exports';
import SnoozePopoverBody from './snooze-popover-body';
@ -12,12 +12,14 @@ class QuickActionSnoozeButton extends Component {
constructor() {
super();
this.openedPopover = false;
}
onClick = (event)=> {
event.stopPropagation()
if (PopoverStore.isPopoverOpen()) {
if (this.openedPopover) {
Actions.closePopover();
this.openedPopover = false;
return;
}
const {thread} = this.props;
@ -25,15 +27,16 @@ class QuickActionSnoozeButton extends Component {
// Grab the parent node because of the zoom applied to this button. If we
// took this element directly, we'd have to divide everything by 2
const element = React.findDOMNode(this).parentNode;
const {height, width, top, left} = element.getBoundingClientRect()
const {height, width, top, bottom, left, right} = element.getBoundingClientRect()
// The parent node is a bit too much to the left, lets adjust this.
const rect = {height, width, top, left: left + 5}
const rect = {height, width, top, bottom, right, left: left + 5}
Actions.openPopover(
<SnoozePopoverBody threads={[thread]}/>,
<SnoozePopoverBody threads={[thread]} closePopover={Actions.closePopover}/>,
rect,
"left"
)
this.openedPopover = true;
};
static containerRequired = false;

View file

@ -53,8 +53,8 @@ export function whenCategoriesReady() {
export function getSnoozeCategory(accountId, categoryName = SNOOZE_CATEGORY_NAME) {
return whenCategoriesReady()
.then(()=> {
const userCategories = CategoryStore.userCategories(accountId)
const category = _.findWhere(userCategories, {displayName: categoryName})
const allCategories = CategoryStore.categories(accountId)
const category = _.findWhere(allCategories, {displayName: categoryName})
if (category) {
return Promise.resolve(category);
}

View file

@ -99,7 +99,7 @@ class SnoozePopoverBody extends Component {
};
updateInputDateValue = _.debounce((dateValue)=> {
const inputDate = DateUtils.fromString(dateValue)
const inputDate = DateUtils.futureDateFromString(dateValue)
this.setState({inputDate})
}, 250);
@ -139,11 +139,11 @@ class SnoozePopoverBody extends Component {
<input
type="text"
tabIndex="1"
placeholder="Or type a time, like 'next monday at 2PM'"
placeholder="Or type a time, like 'next Monday at 2PM'"
onMouseDown={this.onInputMouseDown}
onKeyDown={this.onInputKeyDown}
onChange={this.onInputChange}/>
<em className="input-date-value">{formatted}</em>
<span className="input-date-value">{formatted}</span>
</div>
);
};

View file

@ -1,5 +1,6 @@
/** @babel */
import React, {Component, PropTypes} from 'react';
import {Actions} from 'nylas-exports';
import {Popover} from 'nylas-component-kit';
import SnoozePopoverBody from './snooze-popover-body';
@ -29,7 +30,8 @@ class SnoozePopover extends Component {
direction={direction || 'down-align-left'}
buttonComponent={buttonComponent}
popoverStyle={popoverStyle}
pointerStyle={pointerStyle}>
pointerStyle={pointerStyle}
onOpened={()=> Actions.closePopover()}>
<SnoozePopoverBody threads={threads} closePopover={this.closePopover}/>
</Popover>
);

View file

@ -30,6 +30,7 @@ class SnoozeStore {
})
})
.catch((error)=> {
Actions.closePopover();
NylasEnv.reportError(error);
NylasEnv.showErrorDialog(`Sorry, we were unable to save your snooze settings. ${error.message}`);
});

View file

@ -52,9 +52,6 @@ class DeveloperBar extends React.Component
<span>Requests: {@state.curlHistory.length}</span>
</div>
</div>
<div className="btn-container pull-right">
<div className="btn" onClick={ => Actions.sendFeedback() }>Feedback</div>
</div>
</div>
{@_sectionContent()}
<div className="footer">

View file

@ -90,7 +90,7 @@
{
label: 'Help'
submenu: [
{ label: 'Send Feedback to Nylas', command: 'application:send-feedback' }
{ label: 'Nylas N1 Help', command: 'application:view-help' }
]
}
]

View file

@ -62,7 +62,7 @@
submenu: [
{ label: "VERSION", enabled: false }
{ type: 'separator' }
{ label: 'Send Feedback to Nylas', command: 'application:send-feedback' }
{ label: 'Nylas N1 Help', command: 'application:view-help' }
]
}
]

View file

@ -50,7 +50,7 @@
{ label: 'Check for Update', command: 'application:check-for-update', visible: false}
{ label: 'Downloading Update', enabled: false, visible: false}
{ type: 'separator' }
{ label: 'Send Feedback to Nylas', command: 'application:send-feedback' }
{ label: 'Nylas N1 Help', command: 'application:view-help' }
]
}
{ type: 'separator' }

View file

@ -109,6 +109,7 @@ window.TEST_ACCOUNT_CLIENT_ID = "local-test-account-client-id"
window.TEST_ACCOUNT_ID = "test-account-server-id"
window.TEST_ACCOUNT_EMAIL = "tester@nylas.com"
window.TEST_ACCOUNT_NAME = "Nylas Test"
window.TEST_PLUGIN_ID = "test-plugin-id-123"
beforeEach ->
NylasEnv.testOrganizationUnit = null

View file

@ -273,7 +273,9 @@ class Application
@on 'application:add-account', => @windowManager.ensureOnboardingWindow()
@on 'application:new-message', => @windowManager.sendToMainWindow('new-message')
@on 'application:send-feedback', => @windowManager.sendToMainWindow('send-feedback')
@on 'application:view-help', =>
url = 'https://nylas.zendesk.com/hc/en-us/sections/203638587-N1'
require('electron').shell.openExternal(url)
@on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
@on 'application:show-main-window', => @openWindowsForTokenState()
@on 'application:show-work-window', => @windowManager.showWorkWindow()

View file

@ -215,7 +215,10 @@ class FloatingToolbar extends React.Component
return styles
_renderPointer: =>
unless @state.hidePointer
return <div className="toolbar-pointer" style={@_toolbarPointerStyles()}></div>
return false if @state.hidePointer
<div className="toolbar-pointer-container" style={@_toolbarPointerStyles()}>
<div className="shadow"></div>
<div className="foreground"></div>
</div>
module.exports = FloatingToolbar

View file

@ -2,6 +2,11 @@ import _ from 'underscore';
import React, {Component, PropTypes} from 'react';
// TODO
// This is a temporary hack for the snooze popover
// This should be the actual dimensions of the rendered popover body
const OVERFLOW_LIMIT = 50;
/**
* Renders a popover absultely positioned in the window next to the provided
* rect.
@ -69,12 +74,8 @@ class FixedPopover extends Component {
}
};
_getNewDirection = (direction, originRect, windowDimensions)=> {
// TODO
// This is a temporary solution for the snooze popover.
// This should grab the actual dimensions of the rendered popover body
const limit = 50;
_getNewDirection = (direction, originRect, windowDimensions, limit = OVERFLOW_LIMIT)=> {
// TODO this is a hack. Implement proper repositioning
switch (direction) {
case 'right':
if (
@ -93,7 +94,6 @@ class FixedPopover extends Component {
}
break;
default:
// TODO implement missing
break;
}
return null;
@ -145,9 +145,19 @@ class FixedPopover extends Component {
top: originRect.top,
right: (windowDimensions.width - originRect.left) + 10,
}
// TODO This is a hack for the snooze popover. Fix this
let popoverTop = originRect.height / 2;
let popoverTransform = 'translate(-100%, -50%)';
if (originRect.top < OVERFLOW_LIMIT * 2) {
popoverTop = 0;
popoverTransform = 'translate(-100%, 0)';
} else if (windowDimensions.height - originRect.bottom < OVERFLOW_LIMIT * 2) {
popoverTop = -190;
popoverTransform = 'translate(-100%, 0)';
}
popoverStyle = {
transform: 'translate(-100%, -50%)',
top: originRect.height / 2,
transform: popoverTransform,
top: popoverTop,
}
pointerStyle = {
transform: 'translate(-13px, -50%) rotate(270deg)',
@ -164,7 +174,7 @@ class FixedPopover extends Component {
top: originRect.height / 2,
}
pointerStyle = {
transform: 'translate(-12px, 0) rotate(90deg)',
transform: 'translate(-12px, -50%) rotate(90deg)',
top: originRect.height, // Don't divide by 2 because of zoom
}
break;

View file

@ -0,0 +1,141 @@
import {DraftStore, React, Actions, NylasAPI, APIError, DatabaseStore, Message, Rx} from 'nylas-exports'
import {RetinaImg} from 'nylas-component-kit'
import classnames from 'classnames'
export default class MetadataComposerToggleButton extends React.Component {
static displayName = 'MetadataComposerToggleButton';
static propTypes = {
title: React.PropTypes.func.isRequired,
iconUrl: React.PropTypes.string,
iconName: React.PropTypes.string,
pluginId: React.PropTypes.string.isRequired,
pluginName: React.PropTypes.string.isRequired,
metadataKey: React.PropTypes.string.isRequired,
stickyToggle: React.PropTypes.bool,
errorMessage: React.PropTypes.func.isRequired,
draftClientId: React.PropTypes.string.isRequired,
};
static defaultProps = {
stickyToggle: false,
}
constructor(props) {
super(props);
this.state = {
enabled: false,
isSetup: false,
};
}
componentDidMount() {
this._mounted = true;
const query = DatabaseStore.findBy(Message, {clientId: this.props.draftClientId});
this._subscription = Rx.Observable.fromQuery(query).subscribe(this._onDraftChange)
}
componentWillUnmount() {
this._mounted = false
this._subscription.dispose();
}
_configKey() {
return `plugins.${this.props.pluginId}.defaultOn`
}
_isDefaultOn() {
return NylasEnv.config.get(this._configKey())
}
_onDraftChange = (draft)=> {
if (!this._mounted || !draft) { return; }
const metadata = draft.metadataForPluginId(this.props.pluginId);
if (!metadata) {
if (!this.state.isSetup) {
if (this._isDefaultOn()) {
this._setMetadataValueTo(true)
}
this.setState({isSetup: true})
}
} else {
this.setState({enabled: metadata.tracked, isSetup: true});
}
};
_setMetadataValueTo(enabled) {
const newValue = {}
newValue[this.props.metadataKey] = enabled
this.setState({enabled, pending: true});
const metadataValue = enabled ? newValue : null
// write metadata into the draft to indicate tracked state
return DraftStore.sessionForClientId(this.props.draftClientId).then((session)=> {
const draft = session.draft();
return NylasAPI.authPlugin(this.props.pluginId, this.props.pluginName, draft.accountId)
.then(() => {
Actions.setMetadata(draft, this.props.pluginId, metadataValue);
})
.catch((error) => {
this.setState({enabled: false});
if (this._shouldStickFalseOnError(error)) {
NylasEnv.config.set(this._configKey(), false)
}
let title = "Error"
if (!(error instanceof APIError)) {
NylasEnv.reportError(error);
} else if (error.statusCode === 400) {
NylasEnv.reportError(error);
} else if (error.statusCode === NylasAPI.TimeoutErrorCode) {
title = "Offline"
}
NylasEnv.showErrorDialog({title, message: this.props.errorMessage(error)});
})
}).finally(() => {
this.setState({pending: false})
});
}
_shouldStickFalseOnError(error) {
return this.props.stickyToggle && (error instanceof APIError) &&
(NylasAPI.PermanentErrorCodes.indexOf(error.statusCode) === -1);
}
_onClick = () => {
// Toggle.
if (this.state.pending) { return; }
if (this.props.stickyToggle) {
NylasEnv.config.set(this._configKey(), !this.state.enabled)
}
this._setMetadataValueTo(!this.state.enabled)
};
render() {
const title = this.props.title(this.state.enabled)
const className = classnames({
"btn": true,
"btn-toolbar": true,
"btn-pending": this.state.pending,
"btn-enabled": this.state.enabled,
});
const attrs = {}
if (this.props.iconUrl) {
attrs.url = this.props.iconUrl
} else if (this.props.iconName) {
attrs.name = this.props.iconName
}
return (
<button className={className} onClick={this._onClick} title={title}>
<RetinaImg {...attrs} mode={RetinaImg.Mode.ContentIsMask} />
</button>
);
}
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -126,8 +126,7 @@ class Actions
@longPollOffline: ActionScopeWorkWindow
@willMakeAPIRequest: ActionScopeWorkWindow
@didMakeAPIRequest: ActionScopeWorkWindow
@sendFeedback: ActionScopeWindow
###
Public: Retry the initial sync

View file

@ -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()

View file

@ -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

View file

@ -20,15 +20,23 @@ class PopoverStore extends NylasStore {
this.container = createContainer(containerId);
React.render(<FixedPopover showing={false} />, 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 = (
<FixedPopover
showing
@ -36,19 +44,20 @@ class PopoverStore extends NylasStore {
direction={direction}>
{element}
</FixedPopover>
)
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 = <FixedPopover showing={false} />;
React.render(popover, this.container, ()=> {
this.isOpen = false;
this.trigger();
})
this.renderPopover(popover, false, callback);
};
}

View file

@ -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"

View file

@ -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

View file

@ -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
}

View file

@ -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()

View file

@ -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')

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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;
}
}