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:
Halla Moore 2017-01-15 14:10:04 -08:00
parent 93b4031bcc
commit d3482419f1
8 changed files with 228 additions and 131 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit ae9d6f75defbab23fe5859d313132125844f5266
Subproject commit 32c1caa6c6f38e60e15d1919e71559aec9c19ffc

View file

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

View file

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