Skip to content

Commit

Permalink
Re-implement unsnooze action client-side, clean up lots of plugin code
Browse files Browse the repository at this point in the history
  • Loading branch information
bengotow committed Sep 14, 2017
1 parent 2a31ffd commit 00f658a
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 257 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import {RetinaImg} from 'nylas-component-kit'
import {FocusedPerspectiveStore} from 'nylas-exports'
import {getReminderLabel, getLatestMessage, getLatestMessageWithReminder, setMessageReminder} from './send-reminders-utils'
import {PLUGIN_ID} from './send-reminders-constants'

Expand Down
59 changes: 29 additions & 30 deletions app/internal_packages/thread-snooze/lib/snooze-mail-label.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import {FocusedPerspectiveStore} from 'nylas-exports';
import {RetinaImg, MailLabel} from 'nylas-component-kit';
import {PLUGIN_ID} from './snooze-constants';
import SnoozeUtils from './snooze-utils';
import {snoozedUntilMessage} from './snooze-utils';


class SnoozeMailLabel extends Component {
Expand All @@ -27,37 +27,36 @@ class SnoozeMailLabel extends Component {
}

const {thread} = this.props;
if (thread.categories.find(c => c.role === 'snoozed')) {
let metadata = null;
for (const msg of thread.__messages) {
metadata = msg.metadataForPluginId(PLUGIN_ID);
if (metadata) {
break;
}
}
if (!thread.categories.find(c => c.role === 'snoozed')) {
return false;
}

if (metadata) {
const content = (
<span className="snooze-mail-label">
<RetinaImg
name="icon-snoozed.png"
mode={RetinaImg.Mode.ContentIsMask}
/>
<span className="date-message">
{SnoozeUtils.snoozedUntilMessage(metadata.expiration).replace('Snoozed', '')}
</span>
</span>
)
const label = {
displayName: content,
isLockedCategory: () => true,
hue: () => 259,
}
return <MailLabel label={label} key={`snooze-message-${thread.id}`} />;
}
return <span />
const metadata = thread.metadataForPluginId(PLUGIN_ID);
if (!metadata) {
return false;
}
const content = (
<span className="snooze-mail-label">
<RetinaImg
name="icon-snoozed.png"
mode={RetinaImg.Mode.ContentIsMask}
/>
<span className="date-message">
{snoozedUntilMessage(metadata.expiration).replace('Snoozed', '')}
</span>
</span>
)
const label = {
displayName: content,
isLockedCategory: () => true,
hue: () => 259,
}
return <span />
return (
<MailLabel
label={label}
key={`snooze-message-${thread.id}`}
/>
);
}
}

Expand Down
106 changes: 55 additions & 51 deletions app/internal_packages/thread-snooze/lib/snooze-store.es6
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,37 @@ import {
SyncbackMetadataTask,
Actions,
DatabaseStore,
Message,
Thread,
} from 'nylas-exports';

import SnoozeUtils from './snooze-utils'
import {PLUGIN_ID, PLUGIN_NAME} from './snooze-constants';
import {moveThreads, snoozedUntilMessage} from './snooze-utils'
import {PLUGIN_ID} from './snooze-constants';
import SnoozeActions from './snooze-actions';

class SnoozeStore extends NylasStore {

constructor(pluginId = PLUGIN_ID, pluginName = PLUGIN_NAME) {
super();
this.pluginId = pluginId;
this.pluginName = pluginName;
}

activate() {
this.unsubscribers = [
SnoozeActions.snoozeThreads.listen(this.onSnoozeThreads),
SnoozeActions.snoozeThreads.listen(this._onSnoozeThreads),
DatabaseStore.listen((change) => {
if (change.type !== 'metadata-expiration' || change.objectClass !== Thread.name) {
return;
}
const unsnooze = change.objects.filter((model) => {
const metadata = model.metadataForPluginId(PLUGIN_ID);
return metadata && metadata.expiration && metadata.expiration < new Date();
});
if (unsnooze.length > 0) {
this._onUnsnoozeThreads(unsnooze);
}
}),
];
}

deactivate() {
this.unsubscribers.forEach(unsub => unsub())
}

recordSnoozeEvent(threads, snoozeDate, label) {
_recordSnoozeEvent(threads, snoozeDate, label) {
try {
const timeInSec = Math.round(((new Date(snoozeDate)).valueOf() - Date.now()) / 1000);
Actions.recordUserEvent("Threads Snoozed", {
Expand All @@ -44,21 +49,7 @@ class SnoozeStore extends NylasStore {
}
}

groupUpdatedThreads = (threads) => {
const threadsByAccountId = {}

threads.forEach((thread) => {
const accId = thread.accountId
if (!threadsByAccountId[accId]) {
threadsByAccountId[accId] = [thread];
} else {
threadsByAccountId[accId].threads.push(thread);
}
});
return threadsByAccountId;
};

onSnoozeThreads = async (allThreads, snoozeDate, label) => {
_onSnoozeThreads = async (threads, snoozeDate, label) => {
const lexicon = {
displayName: "Snooze",
usedUpHeader: "All Snoozes used",
Expand All @@ -70,39 +61,52 @@ class SnoozeStore extends NylasStore {
await FeatureUsageStore.asyncUseFeature('snooze', {lexicon});

// log to analytics
this.recordSnoozeEvent(allThreads, snoozeDate, label);

const updatedThreads = await SnoozeUtils.moveThreadsToSnooze(allThreads, snoozeDate);
const updatedThreadsByAccountId = this.groupUpdatedThreads(updatedThreads);

// note we don't wait for this to complete currently
Object.values(updatedThreadsByAccountId).map(async (threads) => {
// Get messages for those threads and metadata for those.
const messages = await DatabaseStore.findAll(Message, {
threadId: threads.map(t => t.id),
});

for (const message of messages) {
Actions.queueTask(new SyncbackMetadataTask({
model: message,
accountId: message.accountId,
pluginId: this.pluginId,
value: {
expiration: snoozeDate,
},
}));
}
});
this._recordSnoozeEvent(threads, snoozeDate, label);

// move the threads to the snoozed folder
await moveThreads(threads, {
snooze: true,
description: snoozedUntilMessage(snoozeDate),
})

// attach metadata to the threads to unsnooze them later
Actions.queueTasks(threads.map((model) =>
new SyncbackMetadataTask({
model,
pluginId: PLUGIN_ID,
value: {
expiration: snoozeDate,
},
}))
);
} catch (error) {
if (error instanceof FeatureUsageStore.NoProAccessError) {
return;
}
SnoozeUtils.moveThreadsFromSnooze(allThreads);
moveThreads(threads, {snooze: false, description: 'Unsnoozed'});
Actions.closePopover();
NylasEnv.reportError(error);
NylasEnv.showErrorDialog(`Sorry, we were unable to save your snooze settings. ${error.message}`);
}
};

_onUnsnoozeThreads = (threads) => {
// move the threads back to the inbox
moveThreads(threads, {snooze: false, description: 'Unsnoozed'});

// remove the expiration on the metadata. note this is super important,
// otherwise we'll receive a notification from the sync worker over and
// over again.
Actions.queueTasks(threads.map((model) =>
new SyncbackMetadataTask({
model,
pluginId: PLUGIN_ID,
value: {
expiration: null,
},
})
));
}
}

export default new SnoozeStore();
Expand Down
95 changes: 32 additions & 63 deletions app/internal_packages/thread-snooze/lib/snooze-utils.es6
Original file line number Diff line number Diff line change
@@ -1,85 +1,54 @@
import moment from 'moment';
import {
Actions,
Thread,
Label,
DateUtils,
TaskFactory,
CategoryStore,
DatabaseStore,
ChangeLabelsTask,
ChangeFolderTask,
TaskQueue,
} from 'nylas-exports';

const {DATE_FORMAT_SHORT} = DateUtils
export function snoozedUntilMessage(snoozeDate, now = moment()) {
let message = 'Snoozed'
if (snoozeDate) {
let dateFormat = DateUtils.DATE_FORMAT_SHORT
const date = moment(snoozeDate)
const hourDifference = moment.duration(date.diff(now)).asHours()

const SnoozeUtils = {
snoozedUntilMessage(snoozeDate, now = moment()) {
let message = 'Snoozed'
if (snoozeDate) {
let dateFormat = DATE_FORMAT_SHORT
const date = moment(snoozeDate)
const hourDifference = moment.duration(date.diff(now)).asHours()

if (hourDifference < 24) {
dateFormat = dateFormat.replace('MMM D, ', '');
}
if (date.minutes() === 0) {
dateFormat = dateFormat.replace(':mm', '');
}

message += ` until ${DateUtils.format(date, dateFormat)}`;
if (hourDifference < 24) {
dateFormat = dateFormat.replace('MMM D, ', '');
}
if (date.minutes() === 0) {
dateFormat = dateFormat.replace(':mm', '');
}
return message;
},

moveThreads(threads, {snooze, description} = {}) {
const tasks = TaskFactory.tasksForThreadsByAccountId(threads, (accountThreads, accountId) => {
const snoozeCat = CategoryStore.getCategoryByRole(accountId, 'snoozed');
const inboxCat = CategoryStore.getInboxCategory(accountId);
message += ` until ${DateUtils.format(date, dateFormat)}`;
}
return message;
}

export function moveThreads(threads, {snooze, description} = {}) {
const tasks = TaskFactory.tasksForThreadsByAccountId(threads, (accountThreads, accountId) => {
const snoozeCat = CategoryStore.getCategoryByRole(accountId, 'snoozed');
const inboxCat = CategoryStore.getInboxCategory(accountId);

if (snoozeCat instanceof Label) {
return new ChangeLabelsTask({
source: "Snooze Move",
threads: accountThreads,
taskDescription: description,
labelsToAdd: snooze ? [snoozeCat] : [inboxCat],
labelsToRemove: snooze ? [inboxCat] : [snoozeCat],
});
}
return new ChangeFolderTask({
if (snoozeCat instanceof Label) {
return new ChangeLabelsTask({
source: "Snooze Move",
threads: accountThreads,
taskDescription: description,
folder: snooze ? snoozeCat : inboxCat,
labelsToAdd: snooze ? [snoozeCat] : [inboxCat],
labelsToRemove: snooze ? [inboxCat] : [snoozeCat],
});
}
return new ChangeFolderTask({
source: "Snooze Move",
threads: accountThreads,
taskDescription: description,
folder: snooze ? snoozeCat : inboxCat,
});
});

Actions.queueTasks(tasks);
const promises = tasks.map(task => TaskQueue.waitForPerformRemote(task))

// Resolve with the updated threads
return (
Promise.all(promises).then(() => {
return DatabaseStore.modelify(Thread, threads.map(t => t.id))
})
)
},

moveThreadsToSnooze(threads, snoozeDate) {
return SnoozeUtils.moveThreads(threads, {
snooze: true,
description: SnoozeUtils.snoozedUntilMessage(snoozeDate),
})
},

moveThreadsFromSnooze(threads) {
return SnoozeUtils.moveThreads(threads, {
snooze: false,
description: 'Unsnoozed',
})
},
Actions.queueTasks(tasks);
}

export default SnoozeUtils
11 changes: 5 additions & 6 deletions app/internal_packages/thread-snooze/specs/snooze-store-spec.es6
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Actions,
Folder,
} from 'nylas-exports'
import SnoozeUtils from '../lib/snooze-utils'
import * as SnoozeUtils from '../lib/snooze-utils'
import SnoozeStore from '../lib/snooze-store'


Expand Down Expand Up @@ -47,8 +47,7 @@ xdescribe('SnoozeStore', function snoozeStore() {

spyOn(AccountStore, 'accountsForItems').andReturn(this.accounts)
spyOn(NylasAPIHelpers, 'authPlugin').andReturn(Promise.resolve())
spyOn(SnoozeUtils, 'moveThreadsToSnooze').andReturn(Promise.resolve(this.threads))
spyOn(SnoozeUtils, 'moveThreadsFromSnooze')
spyOn(SnoozeUtils, 'moveThreads')
spyOn(Actions, 'closePopover')
spyOn(NylasEnv, 'reportError')
spyOn(NylasEnv, 'showErrorDialog')
Expand Down Expand Up @@ -115,16 +114,16 @@ xdescribe('SnoozeStore', function snoozeStore() {
});

it('displays dialog on error', () => {
jasmine.unspy(SnoozeUtils, 'moveThreadsToSnooze')
spyOn(SnoozeUtils, 'moveThreadsToSnooze').andReturn(Promise.reject(new Error('Oh no!')))
jasmine.unspy(SnoozeUtils, 'moveThreads')
spyOn(SnoozeUtils, 'moveThreads').andReturn(Promise.reject(new Error('Oh no!')))

waitsForPromise(async () => {
try {
await this.store.onSnoozeThreads(this.threads, 'date', 'label')
} catch (err) {
//
}
expect(SnoozeUtils.moveThreadsFromSnooze).toHaveBeenCalled()
expect(SnoozeUtils.moveThreads).toHaveBeenCalled()
expect(NylasEnv.reportError).toHaveBeenCalled()
expect(NylasEnv.showErrorDialog).toHaveBeenCalled()
});
Expand Down
Loading

0 comments on commit 00f658a

Please sign in to comment.