mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-22 08:16:09 +08:00
Merge branch 'master' of github.com:nylas/N1
This commit is contained in:
commit
82e674292a
|
@ -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 = {}) ->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}/>
|
||||
|
||||
<RetinaImg name="icon-composer-dropdown.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<RetinaImg name="icon-composer-dropdown.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "feedback",
|
||||
"main": "./lib/main",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"nylas": "*"
|
||||
},
|
||||
"description": "Intercom feeedback",
|
||||
"dependencies": {
|
||||
},
|
||||
"private":true
|
||||
}
|
|
@ -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%);
|
||||
}
|
|
@ -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} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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")];
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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> </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}
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
{
|
||||
label: 'Help'
|
||||
submenu: [
|
||||
{ label: 'Send Feedback to Nylas', command: 'application:send-feedback' }
|
||||
{ label: 'Nylas N1 Help', command: 'application:view-help' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
141
src/components/metadata-composer-toggle-button.jsx
Normal file
141
src/components/metadata-composer-toggle-button.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -126,8 +126,7 @@ class Actions
|
|||
@longPollOffline: ActionScopeWorkWindow
|
||||
@willMakeAPIRequest: ActionScopeWorkWindow
|
||||
@didMakeAPIRequest: ActionScopeWorkWindow
|
||||
@sendFeedback: ActionScopeWindow
|
||||
|
||||
|
||||
###
|
||||
Public: Retry the initial sync
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue