mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-26 14:36:55 +08:00
💄(send-later): Lots of styling of send-later
This commit is contained in:
parent
481dc28585
commit
39265572bc
10 changed files with 188 additions and 184 deletions
|
@ -41,7 +41,9 @@ class InitialPackagesPage extends React.Component
|
|||
<div>
|
||||
{@state.packages.map (item) =>
|
||||
<div className="initial-package" key={item.name}>
|
||||
<img src="nylas://#{item.name}/#{item.icon}" style={width:50} />
|
||||
<div className="icon-container">
|
||||
<img src="nylas://#{item.name}/#{item.icon}" style={width:27, alignContent: 'center', objectFit: 'scale-down'} />
|
||||
</div>
|
||||
<div className="install-container">
|
||||
<InstallButton package={item} />
|
||||
</div>
|
||||
|
|
|
@ -394,8 +394,17 @@
|
|||
border:1px solid #e1e1e1;
|
||||
text-align:left;
|
||||
cursor: default;
|
||||
img {
|
||||
|
||||
.icon-container {
|
||||
float:left;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(to bottom, @background-primary 0%, @background-secondary 100%);
|
||||
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 0.5px 1px rgba(0, 0, 0, 0.15);
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
margin: 12px;
|
||||
margin-left: 5px;
|
||||
margin-right: 20px;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
/** @babel */
|
||||
import _ from 'underscore'
|
||||
import Rx from 'rx-lite'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {DateUtils} from 'nylas-exports'
|
||||
import {Popover} from 'nylas-component-kit'
|
||||
import {DateUtils, Message, DatabaseStore} from 'nylas-exports'
|
||||
import {Popover, RetinaImg, Menu} from 'nylas-component-kit'
|
||||
import SendLaterActions from './send-later-actions'
|
||||
import SendLaterStore from './send-later-store'
|
||||
import {DATE_FORMAT_SHORT, DATE_FORMAT_LONG} from './send-later-constants'
|
||||
|
@ -10,6 +11,7 @@ import {DATE_FORMAT_SHORT, DATE_FORMAT_LONG} from './send-later-constants'
|
|||
|
||||
const SendLaterOptions = {
|
||||
'In 1 hour': DateUtils.in1Hour,
|
||||
'In 2 hours': DateUtils.in2Hours,
|
||||
'Later Today': DateUtils.laterToday,
|
||||
'Tomorrow Morning': DateUtils.tomorrow,
|
||||
'Tomorrow Evening': DateUtils.tomorrowEvening,
|
||||
|
@ -27,128 +29,133 @@ class SendLaterPopover extends Component {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
inputSendDate: null,
|
||||
isScheduled: SendLaterStore.isScheduled(this.props.draftClientId),
|
||||
inputDate: null,
|
||||
scheduledDate: null,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = SendLaterStore.listen(this.onScheduledMessagesChanged)
|
||||
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});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe()
|
||||
this._subscription.dispose();
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
onSendLater = (momentDate)=> {
|
||||
const utcDate = momentDate.utc()
|
||||
const formatted = DateUtils.format(utcDate)
|
||||
SendLaterActions.sendLater(this.props.draftClientId, formatted)
|
||||
onSelectMenuOption = (optionKey)=> {
|
||||
const date = SendLaterOptions[optionKey]();
|
||||
const formatted = DateUtils.format(date.utc())
|
||||
|
||||
this.setState({isScheduled: null, inputSendDate: null})
|
||||
SendLaterActions.sendLater(this.props.draftClientId, formatted)
|
||||
this.setState({scheduledDate: 'saving', inputDate: null})
|
||||
this.refs.popover.close()
|
||||
};
|
||||
|
||||
onCancelSendLater = ()=> {
|
||||
SendLaterActions.cancelSendLater(this.props.draftClientId)
|
||||
this.setState({inputSendDate: null})
|
||||
this.setState({inputDate: null})
|
||||
this.refs.popover.close()
|
||||
};
|
||||
|
||||
onScheduledMessagesChanged = ()=> {
|
||||
const isScheduled = SendLaterStore.isScheduled(this.props.draftClientId)
|
||||
if (isScheduled !== this.state.isScheduled) {
|
||||
this.setState({isScheduled});
|
||||
renderCustomTimeSection() {
|
||||
const updateInputDateValue = _.debounce((value)=> {
|
||||
this.setState({inputDate: DateUtils.fromString(value)})
|
||||
}, 250);
|
||||
|
||||
let dateInterpretation = false;
|
||||
if (this.state.inputDate) {
|
||||
dateInterpretation = (<em>
|
||||
{DateUtils.format(this.state.inputDate, DATE_FORMAT_LONG)}
|
||||
</em>);
|
||||
}
|
||||
};
|
||||
|
||||
onInputChange = (event)=> {
|
||||
this.updateInputSendDateValue(event.target.value)
|
||||
};
|
||||
return (
|
||||
<div key="custom" className="custom-time-section">
|
||||
<input
|
||||
tabIndex={1}
|
||||
type="text"
|
||||
placeholder="Or type a time"
|
||||
onChange={event=> updateInputDateValue(event.target.value)}/>
|
||||
{dateInterpretation}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getButtonLabel = (isScheduled)=> {
|
||||
return isScheduled ? '✅ Scheduled' : 'Send Later';
|
||||
};
|
||||
renderMenuOption(optionKey) {
|
||||
const date = SendLaterOptions[optionKey]();
|
||||
const formatted = DateUtils.format(date, DATE_FORMAT_SHORT);
|
||||
return (
|
||||
<div className="send-later-option">{optionKey}<em>{formatted}</em></div>
|
||||
);
|
||||
}
|
||||
|
||||
updateInputSendDateValue = _.debounce((dateValue)=> {
|
||||
const inputSendDate = DateUtils.fromString(dateValue)
|
||||
this.setState({inputSendDate})
|
||||
}, 250);
|
||||
renderButton() {
|
||||
const {scheduledDate} = this.state;
|
||||
|
||||
renderItems() {
|
||||
return Object.keys(SendLaterOptions).map((label)=> {
|
||||
const date = SendLaterOptions[label]()
|
||||
const formatted = DateUtils.format(date, DATE_FORMAT_SHORT)
|
||||
if (scheduledDate === 'saving') {
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
onMouseDown={this.onSendLater.bind(this, date)}
|
||||
className="send-later-option">
|
||||
{label}
|
||||
<em className="item-date-value">{formatted}</em>
|
||||
</div>
|
||||
<RetinaImg
|
||||
name="inline-loading-spinner.gif"
|
||||
mode={RetinaImg.Mode.ContentDark}
|
||||
style={{width: 14, height: 14}}/>
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
renderEmptyInput() {
|
||||
let className = 'btn btn-toolbar btn-send-later';
|
||||
let dateInterpretation = false;
|
||||
if (scheduledDate) {
|
||||
className += ' scheduled';
|
||||
const momentDate = DateUtils.fromString(scheduledDate);
|
||||
if (momentDate) {
|
||||
dateInterpretation = <span className="at">Sending in {momentDate.fromNow(true)}</span>;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="send-later-section">
|
||||
<label>At a specific time</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Next Monday at 1pm"
|
||||
onChange={this.onInputChange}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderLabeledInput(inputSendDate) {
|
||||
const formatted = DateUtils.format(inputSendDate, DATE_FORMAT_LONG)
|
||||
return (
|
||||
<div className="send-later-section">
|
||||
<label>At a specific time</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Next Monday at 1pm"
|
||||
onChange={this.onInputChange}/>
|
||||
<em className="input-date-value">{formatted}</em>
|
||||
<button
|
||||
className="btn btn-send-later"
|
||||
onClick={this.onSendLater.bind(this, inputSendDate)}>Schedule Email</button>
|
||||
</div>
|
||||
)
|
||||
<button className={className}>
|
||||
<RetinaImg name="icon-composer-sendlater.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
{dateInterpretation}
|
||||
<RetinaImg name="composer-caret.png" style={{marginLeft: 6}} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isScheduled, inputSendDate} = this.state
|
||||
const buttonLabel = isScheduled != null ? this.getButtonLabel(isScheduled) : 'Scheduling...';
|
||||
const button = (
|
||||
<button className="btn btn-primary send-later-button">{buttonLabel}</button>
|
||||
)
|
||||
const input = inputSendDate ? this.renderLabeledInput(inputSendDate) : this.renderEmptyInput();
|
||||
const footerComponents = [
|
||||
<div key="divider" className="divider" />,
|
||||
this.renderCustomTimeSection(),
|
||||
];
|
||||
|
||||
if (this.state.scheduledDate) {
|
||||
footerComponents.push(<div key="divider-unschedule" className="divider" />)
|
||||
footerComponents.push(
|
||||
<div className="cancel-section" key="cancel-section">
|
||||
<button className="btn" onClick={this.onCancelSendLater}>
|
||||
Unschedule Send
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
ref="popover"
|
||||
style={{order: -103}}
|
||||
className="send-later"
|
||||
buttonComponent={button}>
|
||||
<div className="send-later-container">
|
||||
{this.renderItems()}
|
||||
<div className="divider" />
|
||||
{input}
|
||||
{isScheduled ?
|
||||
<div className="divider" />
|
||||
: void 0}
|
||||
{isScheduled ?
|
||||
<div className="send-later-section">
|
||||
<button className="btn btn-send-later" onClick={this.onCancelSendLater}>
|
||||
Unschedule Send
|
||||
</button>
|
||||
</div>
|
||||
: void 0}
|
||||
</div>
|
||||
buttonComponent={this.renderButton()}>
|
||||
<Menu items={ Object.keys(SendLaterOptions) }
|
||||
itemKey={ (item)=> item }
|
||||
itemContent={this.renderMenuOption}
|
||||
footerComponents={footerComponents}
|
||||
onSelect={this.onSelectMenuOption}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,61 +9,36 @@ class SendLaterStore extends NylasStore {
|
|||
|
||||
constructor(pluginId = PLUGIN_ID) {
|
||||
super()
|
||||
this.pluginId = pluginId
|
||||
this.scheduledMessages = new Map()
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
|
||||
activate() {
|
||||
this.setupQuerySubscription()
|
||||
|
||||
this.unsubscribers = [
|
||||
SendLaterActions.sendLater.listen(this.onSendLater),
|
||||
SendLaterActions.cancelSendLater.listen(this.onCancelSendLater),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
setupQuerySubscription() {
|
||||
const query = DatabaseStore.findAll(
|
||||
Message, [Message.attributes.pluginMetadata.contains(this.pluginId)]
|
||||
)
|
||||
this.queryDisposable = Rx.Observable.fromQuery(query).subscribe(this.onScheduledMessagesChanged)
|
||||
}
|
||||
|
||||
getScheduledMessage = (messageClientId)=> {
|
||||
return this.scheduledMessages.get(messageClientId)
|
||||
};
|
||||
|
||||
isScheduled = (messageClientId)=> {
|
||||
const message = this.getScheduledMessage(messageClientId)
|
||||
if (message && message.metadataForPluginId(this.pluginId).sendLaterDate) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
getScheduledDateForMessage = (message)=> {
|
||||
const metadata = message.metadataForPluginId(this.pluginId) || {};
|
||||
return metadata.sendLaterDate || null;
|
||||
};
|
||||
|
||||
setMetadata = (draftClientId, metadata)=> {
|
||||
return (
|
||||
DatabaseStore.modelify(Message, [draftClientId])
|
||||
.then((messages)=> {
|
||||
const {accountId} = messages[0]
|
||||
return NylasAPI.authPlugin(this.pluginId, PLUGIN_NAME, accountId)
|
||||
.then(()=> {
|
||||
Actions.setMetadata(messages, this.pluginId, metadata)
|
||||
})
|
||||
.catch((error)=> {
|
||||
console.error(error)
|
||||
NylasEnv.showErrorDialog(error.message)
|
||||
})
|
||||
const {accountId} = messages[0];
|
||||
return NylasAPI.authPlugin(this.pluginId, PLUGIN_NAME, accountId);
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
onScheduledMessagesChanged = (messages)=> {
|
||||
this.scheduledMessages.clear()
|
||||
messages.forEach((message)=> {
|
||||
this.scheduledMessages.set(message.clientId, message);
|
||||
})
|
||||
this.trigger()
|
||||
.then(()=> {
|
||||
Actions.setMetadata(messages, this.pluginId, metadata);
|
||||
})
|
||||
.catch((error)=> {
|
||||
NylasEnv.reportError(error);
|
||||
NylasEnv.showErrorDialog(error.message);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onSendLater = (draftClientId, sendLaterDate)=> {
|
||||
|
@ -75,7 +50,6 @@ class SendLaterStore extends NylasStore {
|
|||
};
|
||||
|
||||
deactivate = ()=> {
|
||||
this.queryDisposable.dispose()
|
||||
this.unsubscribers.forEach(unsub => unsub())
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,60 +1,61 @@
|
|||
@import "ui-variables";
|
||||
|
||||
|
||||
.send-later {
|
||||
em {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.send-later-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 0;
|
||||
.menu {
|
||||
width: 250px;
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid @border-color-divider;
|
||||
margin: 10px 0;
|
||||
width: 90%;
|
||||
align-self: center;
|
||||
.header-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.send-later-section {
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
input {
|
||||
border: 1px solid @input-border;
|
||||
}
|
||||
.input-date-value {
|
||||
font-size: 0.9em;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.btn-send-later {
|
||||
width: 100%;
|
||||
}
|
||||
.content-container {
|
||||
overflow:hidden;
|
||||
border-top-right-radius: @border-radius-base;
|
||||
border-top-left-radius: @border-radius-base;
|
||||
}
|
||||
|
||||
.send-later-option {
|
||||
cursor: default;
|
||||
width: 100%;
|
||||
padding: 1px 10px;
|
||||
|
||||
.item-date-value {
|
||||
.item {
|
||||
em {
|
||||
display: none;
|
||||
float: right;
|
||||
font-size: 0.9em;
|
||||
padding-right: @padding-base-horizontal;
|
||||
}
|
||||
&:hover {
|
||||
background-color: @background-secondary;
|
||||
.item-date-value {
|
||||
em {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
border-top: 1px solid @border-color-divider;
|
||||
}
|
||||
.custom-time-section {
|
||||
padding: @padding-base-vertical @padding-base-horizontal;
|
||||
}
|
||||
.cancel-section {
|
||||
padding: @padding-base-vertical @padding-base-horizontal;
|
||||
padding-bottom: @padding-base-vertical * 1.2;
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-send-later {
|
||||
&.scheduled {
|
||||
img { background-color: @component-active-color; margin-right: 5px; }
|
||||
color: @component-active-color;
|
||||
.at {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.send-later-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -45,6 +45,10 @@ const DateUtils = {
|
|||
return DateUtils.minutesFromNow(60);
|
||||
},
|
||||
|
||||
in2Hours() {
|
||||
return DateUtils.minutesFromNow(120);
|
||||
},
|
||||
|
||||
laterToday(now = moment()) {
|
||||
return now.add(3, 'hours').oclock();
|
||||
},
|
||||
|
|
|
@ -11,24 +11,31 @@
|
|||
border-bottom: 1px solid @base-border-color;
|
||||
padding: 3px;
|
||||
position: relative;
|
||||
input.search {
|
||||
border: 1px solid darken(@background-secondary, 10%);
|
||||
border-radius: 3px;
|
||||
padding-left: 0;
|
||||
background-color: white;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("../static/images/search/searchloupe@2x.png");
|
||||
background-size: 15px 15px;
|
||||
background-position: 7px 4px;
|
||||
text-indent: 31px;
|
||||
box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05)
|
||||
}
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-container,
|
||||
.footer-container {
|
||||
input[type=text] {
|
||||
border: 1px solid darken(@background-secondary, 10%);
|
||||
border-radius: 3px;
|
||||
background-color: white;
|
||||
box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05);
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
background: @background-secondary;
|
||||
width: 100%;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
}
|
||||
|
||||
.popover {
|
||||
background-color: @background-primary;
|
||||
background-color: @background-secondary;
|
||||
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);
|
||||
.menu {
|
||||
|
@ -34,7 +34,7 @@
|
|||
|
||||
.popover-pointer {
|
||||
-webkit-mask-image: url('images/tooltip/tooltip-bg-pointer@2x.png');
|
||||
background-color: @background-primary;
|
||||
background-color: @background-secondary;
|
||||
}
|
||||
.popover-pointer.shadow {
|
||||
-webkit-mask-image: url('images/tooltip/tooltip-bg-pointer-shadow@2x.png');
|
||||
|
|
BIN
static/images/composer/icon-composer-eye@2x.png
Normal file
BIN
static/images/composer/icon-composer-eye@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
static/images/composer/icon-composer-sendlater@2x.png
Normal file
BIN
static/images/composer/icon-composer-sendlater@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Loading…
Add table
Reference in a new issue