mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-23 07:36:12 +08:00
feat(sync-status): Hide syncback tasks inside the expanded details
Summary: Except for send tasks, place notifications of syncback tasks inside the expanded sync details. Addresses T7458 Test Plan: tested locally Reviewers: juan, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D3693
This commit is contained in:
parent
93b4031bcc
commit
d3482419f1
8 changed files with 228 additions and 131 deletions
|
@ -4,15 +4,19 @@ ReactCSSTransitionGroup = require 'react-addons-css-transition-group'
|
|||
_ = require 'underscore'
|
||||
classNames = require 'classnames'
|
||||
|
||||
StreamingSyncActivity = require './streaming-sync-activity'
|
||||
InitialSyncActivity = require('./initial-sync-activity').default
|
||||
SyncActivity = require("./sync-activity").default
|
||||
SyncbackActivity = require("./syncback-activity").default
|
||||
|
||||
{Utils,
|
||||
Actions,
|
||||
TaskQueue,
|
||||
AccountStore,
|
||||
NylasSyncStatusStore,
|
||||
TaskQueueStatusStore} = require 'nylas-exports'
|
||||
TaskQueueStatusStore
|
||||
PerformSendActionTask,
|
||||
SendDraftTask} = require 'nylas-exports'
|
||||
|
||||
SEND_TASK_CLASSES = [PerformSendActionTask, SendDraftTask]
|
||||
|
||||
class ActivitySidebar extends React.Component
|
||||
@displayName: 'ActivitySidebar'
|
||||
|
@ -38,12 +42,14 @@ class ActivitySidebar extends React.Component
|
|||
unlisten() for unlisten in @_unlisteners
|
||||
|
||||
render: =>
|
||||
items = @_renderTaskActivityItems()
|
||||
sendTasks = []
|
||||
nonSendTasks = []
|
||||
@state.tasks.forEach (task) ->
|
||||
if SEND_TASK_CLASSES.some(((taskClass) -> task instanceof taskClass ))
|
||||
sendTasks.push(task)
|
||||
else
|
||||
nonSendTasks.push(task)
|
||||
|
||||
if @state.isInitialSyncComplete
|
||||
items.push <StreamingSyncActivity key="streaming-sync" />
|
||||
else
|
||||
items.push <InitialSyncActivity key="initial-sync-activity" />
|
||||
|
||||
names = classNames
|
||||
"sidebar-activity": true
|
||||
|
@ -51,16 +57,17 @@ class ActivitySidebar extends React.Component
|
|||
|
||||
wrapperClass = "sidebar-activity-transition-wrapper "
|
||||
|
||||
if items.length is 0
|
||||
wrapperClass += "sidebar-activity-empty"
|
||||
else
|
||||
inside = <ReactCSSTransitionGroup
|
||||
className={names}
|
||||
transitionLeaveTimeout={625}
|
||||
transitionEnterTimeout={125}
|
||||
transitionName="activity-opacity">
|
||||
{items}
|
||||
</ReactCSSTransitionGroup>
|
||||
inside = <ReactCSSTransitionGroup
|
||||
className={names}
|
||||
transitionLeaveTimeout={625}
|
||||
transitionEnterTimeout={125}
|
||||
transitionName="activity-opacity">
|
||||
<SyncbackActivity syncbackTasks={sendTasks} />
|
||||
<SyncActivity
|
||||
initialSync={!@state.isInitialSyncComplete}
|
||||
syncbackTasks={nonSendTasks}
|
||||
/>
|
||||
</ReactCSSTransitionGroup>
|
||||
|
||||
<ReactCSSTransitionGroup
|
||||
className={wrapperClass}
|
||||
|
@ -70,23 +77,6 @@ class ActivitySidebar extends React.Component
|
|||
{inside}
|
||||
</ReactCSSTransitionGroup>
|
||||
|
||||
_renderTaskActivityItems: =>
|
||||
summary = {}
|
||||
|
||||
@state.tasks.map (task) ->
|
||||
label = task.label?()
|
||||
return unless label
|
||||
summary[label] ?= 0
|
||||
summary[label] += task.numberOfImpactedItems()
|
||||
|
||||
_.pairs(summary).map ([label, count]) ->
|
||||
<div className="item" key={label}>
|
||||
<div className="inner">
|
||||
<span className="count">({new Number(count).toLocaleString()})</span>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_onDataChanged: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from 'underscore';
|
||||
import _str from 'underscore.string';
|
||||
import classNames from 'classnames';
|
||||
import {Utils, Actions, AccountStore, NylasSyncStatusStore, React} from 'nylas-exports';
|
||||
import {Utils, AccountStore, NylasSyncStatusStore, React} from 'nylas-exports';
|
||||
|
||||
const MONTH_SHORT_FORMATS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
|
||||
'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
@ -12,7 +11,6 @@ export default class InitialSyncActivity extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExpanded: false,
|
||||
syncState: NylasSyncStatusStore.getSyncState(),
|
||||
syncProgress: NylasSyncStatusStore.getSyncProgress(),
|
||||
}
|
||||
|
@ -21,10 +19,7 @@ export default class InitialSyncActivity extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.unsubs = [
|
||||
NylasSyncStatusStore.listen(this.onDataChanged),
|
||||
Actions.expandInitialSyncState.listen(this.showExpandedState),
|
||||
]
|
||||
this.unsub = NylasSyncStatusStore.listen(this.onDataChanged)
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -33,9 +28,7 @@ export default class InitialSyncActivity extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.unsubs) {
|
||||
this.unsubs.forEach((unsub) => unsub())
|
||||
}
|
||||
this.unsub();
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
|
@ -45,24 +38,42 @@ export default class InitialSyncActivity extends React.Component {
|
|||
this.setState({syncState, syncProgress});
|
||||
}
|
||||
|
||||
hideExpandedState = () => {
|
||||
this.setState({isExpanded: false});
|
||||
}
|
||||
|
||||
showExpandedState = () => {
|
||||
if (!this.state.isExpanded) {
|
||||
this.setState({isExpanded: true});
|
||||
renderFolderProgress(name, progress, oldestProcessedDate) {
|
||||
let status = 'busy';
|
||||
let progressLabel = 'In Progress'
|
||||
let syncedThrough = 'Syncing this past month';
|
||||
if (progress === 1) {
|
||||
status = 'complete';
|
||||
progressLabel = '';
|
||||
syncedThrough = 'Up to date'
|
||||
} else {
|
||||
this.setState({blink: true});
|
||||
setTimeout(() => {
|
||||
if (this.mounted) {
|
||||
this.setState({blink: false});
|
||||
let month = oldestProcessedDate.getMonth();
|
||||
let year = oldestProcessedDate.getFullYear();
|
||||
const currentDate = new Date();
|
||||
if (month !== currentDate.getMonth() || year !== currentDate.getFullYear()) {
|
||||
// We're currently syncing in `month`, which mean's we've synced through all
|
||||
// of the month *after* it.
|
||||
month++;
|
||||
if (month === 12) {
|
||||
month = 0;
|
||||
year++;
|
||||
}
|
||||
}, 1000)
|
||||
syncedThrough = `Synced through ${MONTH_SHORT_FORMATS[month]} ${year}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`model-progress ${status}`} key={name} title={syncedThrough}>
|
||||
{_str.titleize(name)} <span className="progress-label">{progressLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderExpandedSyncState() {
|
||||
render() {
|
||||
if (!AccountStore.accountsAreSyncing() || this.state.syncProgress.progress === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let maxHeight = 0;
|
||||
let accounts = _.map(this.state.syncState, (accountSyncState, accountId) => {
|
||||
const account = _.findWhere(AccountStore.accounts(), {id: accountId});
|
||||
|
@ -101,70 +112,9 @@ export default class InitialSyncActivity extends React.Component {
|
|||
key="expanded-sync-state"
|
||||
style={{maxHeight: `${maxHeight + 500}px`}}
|
||||
>
|
||||
<a className="close-expanded" onClick={this.hideExpandedState}>
|
||||
<span style={{cursor: "pointer"}}>Hide</span>
|
||||
</a>
|
||||
{accounts}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderFolderProgress(name, progress, oldestProcessedDate) {
|
||||
let status = 'busy';
|
||||
let progressLabel = 'In Progress'
|
||||
let syncedThrough = 'Syncing this past month';
|
||||
if (progress === 1) {
|
||||
status = 'complete';
|
||||
progressLabel = '';
|
||||
syncedThrough = 'Up to date'
|
||||
} else {
|
||||
let month = oldestProcessedDate.getMonth();
|
||||
let year = oldestProcessedDate.getFullYear();
|
||||
const currentDate = new Date();
|
||||
if (month !== currentDate.getMonth() || year !== currentDate.getFullYear()) {
|
||||
// We're currently syncing in `month`, which mean's we've synced through all
|
||||
// of the month *after* it.
|
||||
month++;
|
||||
if (month === 12) {
|
||||
month = 0;
|
||||
year++;
|
||||
}
|
||||
syncedThrough = `Synced through ${MONTH_SHORT_FORMATS[month]} ${year}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`model-progress ${status}`} key={name} title={syncedThrough}>
|
||||
{_str.titleize(name)} <span className="progress-label">{progressLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!AccountStore.accountsAreSyncing()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {syncProgress: {progress}} = this.state
|
||||
if (progress === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const classSet = classNames({
|
||||
'item': true,
|
||||
'expanded-sync': this.state.isExpanded,
|
||||
'blink': this.state.blink,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classSet}
|
||||
key="initial-sync"
|
||||
onClick={() => (this.setState({isExpanded: !this.state.isExpanded}))}
|
||||
>
|
||||
<div className="inner">Syncing your mailbox</div>
|
||||
{this.state.isExpanded ? this.renderExpandedSyncState() : false}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import classNames from 'classnames';
|
||||
import {Actions, React, Utils} from 'nylas-exports';
|
||||
|
||||
import InitialSyncActivity from './initial-sync-activity';
|
||||
import SyncbackActivity from './syncback-activity';
|
||||
|
||||
export default class SyncActivity extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
initialSync: React.PropTypes.bool,
|
||||
syncbackTasks: React.PropTypes.array,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
expanded: false,
|
||||
blink: false,
|
||||
}
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.unsub = Actions.expandInitialSyncState.listen(this.showExpandedState);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !Utils.isEqualReact(nextProps, this.props) ||
|
||||
!Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.unsub();
|
||||
}
|
||||
|
||||
showExpandedState = () => {
|
||||
if (!this.state.expanded) {
|
||||
this.setState({expanded: true});
|
||||
} else {
|
||||
this.setState({blink: true});
|
||||
setTimeout(() => {
|
||||
if (this.mounted) {
|
||||
this.setState({blink: false});
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
hideExpandedState = () => {
|
||||
this.setState({expanded: false});
|
||||
}
|
||||
|
||||
_renderInitialSync() {
|
||||
if (!this.props.initialSync) { return false; }
|
||||
return <InitialSyncActivity />
|
||||
}
|
||||
|
||||
_renderSyncbackTasks() {
|
||||
return <SyncbackActivity syncbackTasks={this.props.syncbackTasks} />
|
||||
}
|
||||
|
||||
_renderExpandedDetails() {
|
||||
return (
|
||||
<div>
|
||||
<a className="close-expanded" onClick={this.hideExpandedState}>Hide</a>
|
||||
{this._renderSyncbackTasks()}
|
||||
{this._renderInitialSync()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {initialSync, syncbackTasks} = this.props;
|
||||
if (!initialSync && (!syncbackTasks || syncbackTasks.length === 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const classSet = classNames({
|
||||
'item': true,
|
||||
'expanded-sync': this.state.expanded,
|
||||
'blink': this.state.blink,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classSet}
|
||||
key="sync-activity"
|
||||
onClick={() => (this.setState({expanded: !this.state.expanded}))}
|
||||
>
|
||||
<div className="inner clickable">Syncing your mailbox</div>
|
||||
{this.state.expanded ? this._renderExpandedDetails() : false}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import _ from 'underscore';
|
||||
import {React, Utils} from 'nylas-exports';
|
||||
|
||||
export default class SyncbackActivity extends React.Component {
|
||||
static propTypes = {
|
||||
syncbackTasks: React.PropTypes.array,
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !Utils.isEqualReact(nextProps, this.props) ||
|
||||
!Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {syncbackTasks} = this.props;
|
||||
if (!syncbackTasks || syncbackTasks.length === 0) { return false; }
|
||||
|
||||
const counts = {}
|
||||
this.props.syncbackTasks.forEach((task) => {
|
||||
const label = task.label ? task.label() : null;
|
||||
if (!label) { return; }
|
||||
if (!counts[label]) {
|
||||
counts[label] = 0;
|
||||
}
|
||||
counts[label] += +task.numberOfImpactedItems()
|
||||
});
|
||||
|
||||
const items = _.pairs(counts).map(([label, count]) => {
|
||||
return (
|
||||
<div className="item" key={label}>
|
||||
<div className="inner">
|
||||
<span className="count">({count.toLocaleString()})</span>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push(
|
||||
<div className="item" key="no-labels">
|
||||
<div className="inner">
|
||||
Applying tasks
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,12 +5,6 @@
|
|||
order: 2;
|
||||
z-index: 2;
|
||||
overflow-y: auto;
|
||||
|
||||
&.sidebar-activity-empty {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-activity {
|
||||
|
@ -24,12 +18,14 @@
|
|||
box-shadow:inset 0 1px 0 @border-color-divider;
|
||||
&:hover { cursor: default }
|
||||
|
||||
|
||||
.item {
|
||||
border-bottom:1px solid @border-color-divider;
|
||||
&:hover { cursor: default }
|
||||
.clickable { cursor: pointer; }
|
||||
|
||||
.inner {
|
||||
padding: @padding-large-vertical @padding-base-horizontal @padding-large-vertical @padding-base-horizontal;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
.inner::after {
|
||||
content: '...';
|
||||
|
@ -115,10 +111,11 @@
|
|||
color: @text-color-subtle;
|
||||
}
|
||||
.close-expanded {
|
||||
display: block;
|
||||
text-align: right;
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
padding: @padding-large-vertical @padding-base-horizontal 0 @padding-base-horizontal;
|
||||
padding: @padding-large-vertical @padding-base-horizontal;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
2
src/K2
2
src/K2
|
@ -1 +1 @@
|
|||
Subproject commit ae9d6f75defbab23fe5859d313132125844f5266
|
||||
Subproject commit 32c1caa6c6f38e60e15d1919e71559aec9c19ffc
|
|
@ -10,6 +10,10 @@ export default class ChangeUnreadTask extends ChangeMailTask {
|
|||
this.unread = options.unread;
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.unread ? "Marking as unread" : "Marking as read";
|
||||
}
|
||||
|
||||
description() {
|
||||
const count = this.threads.length;
|
||||
const type = count > 1 ? 'threads' : 'thread';
|
||||
|
|
|
@ -13,6 +13,10 @@ export default class EnsureMessageInSentFolderTask extends Task {
|
|||
this.sentPerRecipient = opts.sentPerRecipient;
|
||||
}
|
||||
|
||||
label() {
|
||||
return "Saving to sent folder";
|
||||
}
|
||||
|
||||
isDependentOnTask(other) {
|
||||
return (other instanceof SendDraftTask) && (other.message) && (other.message.clientId === this.message.clientId);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue