diff --git a/app/internal_packages/composer/lib/send-action-button.jsx b/app/internal_packages/composer/lib/send-action-button.jsx index 646a406bc..fca8134d3 100644 --- a/app/internal_packages/composer/lib/send-action-button.jsx +++ b/app/internal_packages/composer/lib/send-action-button.jsx @@ -1,4 +1,4 @@ -import { React, PropTypes, Actions, SendActionsStore } from 'mailspring-exports'; +import { React, PropTypes, Actions, SendActionsStore, SoundRegistry } from 'mailspring-exports'; import { Menu, RetinaImg, ButtonDropdown, ListensToFluxStore } from 'mailspring-component-kit'; class SendActionButton extends React.Component { @@ -26,6 +26,9 @@ class SendActionButton extends React.Component { _onSendWithAction = sendAction => { const { isValidDraft, draft } = this.props; if (isValidDraft()) { + if (AppEnv.config.get('core.sending.sounds')) { + SoundRegistry.playSound('hit-send'); + } Actions.sendDraft(draft.headerMessageId, sendAction.configKey); } }; diff --git a/app/internal_packages/send-reminders/lib/send-reminders-store.es6 b/app/internal_packages/send-reminders/lib/send-reminders-store.es6 index 76a310ed9..c8820f65b 100644 --- a/app/internal_packages/send-reminders/lib/send-reminders-store.es6 +++ b/app/internal_packages/send-reminders/lib/send-reminders-store.es6 @@ -1,12 +1,9 @@ import { Actions, FocusedContentStore, - SyncbackDraftTask, + SendDraftTask, DatabaseStore, - AccountStore, - TaskQueue, Thread, - Contact, DraftFactory, } from 'mailspring-exports'; import MailspringStore from 'mailspring-store'; @@ -36,26 +33,13 @@ class SendRemindersStore extends MailspringStore { } _sendReminderEmail = async (thread, sentHeaderMessageId) => { - const account = AccountStore.accountForId(thread.accountId); - const draft = await DraftFactory.createDraft({ - from: [new Contact({ email: account.emailAddress, name: `${account.name} via Mailspring` })], - to: [account.defaultMe()], - cc: [], - pristine: false, - subject: thread.subject, - threadId: thread.id, - accountId: thread.accountId, - replyToHeaderMessageId: sentHeaderMessageId, - body: ` - Mailspring Reminder: This thread has been moved to the top of - your inbox by Mailspring because no one has replied to your message.

-

--The Mailspring Team

`, - }); + const body = ` + Mailspring Reminder: This thread has been moved to the top of + your inbox by Mailspring because no one has replied to your message.

+

--The Mailspring Team

`; - const saveTask = new SyncbackDraftTask({ draft }); - Actions.queueTask(saveTask); - await TaskQueue.waitForPerformLocal(saveTask); - Actions.sendDraft(draft.headerMessageId); + const draft = await DraftFactory.createDraftForResurfacing(thread, sentHeaderMessageId, body); + Actions.queueTask(new SendDraftTask({ draft, silent: true })); }; _onDraftDeliverySucceeded = ({ headerMessageId, accountId }) => { diff --git a/app/internal_packages/thread-snooze/lib/snooze-mail-label.jsx b/app/internal_packages/thread-snooze/lib/snooze-mail-label.jsx index b054ba8c4..f493ba9da 100644 --- a/app/internal_packages/thread-snooze/lib/snooze-mail-label.jsx +++ b/app/internal_packages/thread-snooze/lib/snooze-mail-label.jsx @@ -29,7 +29,7 @@ class SnoozeMailLabel extends Component { } const metadata = thread.metadataForPluginId(PLUGIN_ID); - if (!metadata) { + if (!metadata || !metadata.expiration) { return false; } const content = ( diff --git a/app/internal_packages/thread-snooze/lib/snooze-store.es6 b/app/internal_packages/thread-snooze/lib/snooze-store.es6 index ae4cf1b66..700b5b095 100644 --- a/app/internal_packages/thread-snooze/lib/snooze-store.es6 +++ b/app/internal_packages/thread-snooze/lib/snooze-store.es6 @@ -8,7 +8,7 @@ import { Thread, } from 'mailspring-exports'; -import { markUnreadIfSet, moveThreads, snoozedUntilMessage } from './snooze-utils'; +import { markUnreadOrResurfaceThreads, moveThreads, snoozedUntilMessage } from './snooze-utils'; import { PLUGIN_ID } from './snooze-constants'; import SnoozeActions from './snooze-actions'; @@ -92,23 +92,7 @@ class SnoozeStore extends MailspringStore { moveThreads(threads, { snooze: false, description: 'Unsnoozed' }); // mark the threads unread if setting is enabled - markUnreadIfSet(threads, 'Unsnoozed message'); - - // 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, - }, - }) - ) - ); + markUnreadOrResurfaceThreads(threads, 'Unsnoozed message'); }; _onMetadataExpired = threads => { @@ -117,7 +101,25 @@ class SnoozeStore extends MailspringStore { return metadata && metadata.expiration && metadata.expiration < new Date(); }); if (unsnooze.length > 0) { - this._onUnsnoozeThreads(unsnooze); + // 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, + }, + }) + ) + ); + + // unsnooze messages that are still in the snoozed folder. (The user may have + // moved the thread out of the snoozed folder using another client ) + this._onUnsnoozeThreads(unsnooze.filter(t => t.categories.find(c => c.role === 'snoozed'))); } }; } diff --git a/app/internal_packages/thread-snooze/lib/snooze-utils.es6 b/app/internal_packages/thread-snooze/lib/snooze-utils.es6 index 68b97f52d..79dab5219 100644 --- a/app/internal_packages/thread-snooze/lib/snooze-utils.es6 +++ b/app/internal_packages/thread-snooze/lib/snooze-utils.es6 @@ -7,6 +7,8 @@ import { CategoryStore, ChangeLabelsTask, ChangeFolderTask, + DraftFactory, + SendDraftTask, } from 'mailspring-exports'; export function snoozedUntilMessage(snoozeDate, now = moment()) { @@ -40,6 +42,7 @@ export function moveThreads(threads, { snooze, description } = {}) { taskDescription: description, labelsToAdd: snooze ? [snoozeCat] : [inboxCat], labelsToRemove: snooze ? [inboxCat] : [snoozeCat], + canBeUndone: snooze ? true : false, }); } return new ChangeFolderTask({ @@ -47,20 +50,37 @@ export function moveThreads(threads, { snooze, description } = {}) { threads: accountThreads, taskDescription: description, folder: snooze ? snoozeCat : inboxCat, + canBeUndone: snooze ? true : false, }); }); Actions.queueTasks(tasks); } -export function markUnreadIfSet(threads, source) { - if (AppEnv.config.get('core.notifications.unreadOnSnooze')) { +export async function markUnreadOrResurfaceThreads(threads, source) { + if (AppEnv.config.get('core.notifications.unsnoozeToTop')) { + // send a hidden email that will mark the thread as unread and bring it + // to the top of your inbox in any mail client + const body = ` + Mailspring Reminder: This thread has been moved to the top of + your inbox by Mailspring.

+

--The Mailspring Team

`; + + for (const thread of threads) { + const draft = await DraftFactory.createDraftForResurfacing(thread, null, body); + Actions.queueTask(new SendDraftTask({ draft, silent: true })); + } + } else { + // just mark the threads as unread (unless they're all already unread) + if (!threads.some(t => !t.unread)) { + return; + } Actions.queueTask( TaskFactory.taskForSettingUnread({ unread: true, threads: threads, source: source, - canBeUndone: true, + canBeUndone: false, }) ); } diff --git a/app/src/config-schema.es6 b/app/src/config-schema.es6 index 01c65dee8..2443dd76b 100644 --- a/app/src/config-schema.es6 +++ b/app/src/config-schema.es6 @@ -158,10 +158,10 @@ export default { default: true, title: 'Play sound when receiving new mail', }, - unreadOnSnooze: { + unsnoozeToTop: { type: 'boolean', - default: false, - title: 'Mark a message unread when returning from snooze', + default: true, + title: 'Resurface messages to the top of the inbox when unsnoozing', }, countBadge: { type: 'string', diff --git a/app/src/flux/models/message.es6 b/app/src/flux/models/message.es6 index d028f8b8a..68490e894 100644 --- a/app/src/flux/models/message.es6 +++ b/app/src/flux/models/message.es6 @@ -351,7 +351,7 @@ export default class Message extends ModelWithMetadata { this.to.length === 1 && this.from.length === 1 && this.to[0].email === this.from[0].email && - (this.snippet || '').startsWith('Mailspring Reminder:'); + (this.from[0].name || '').endsWith('via Mailspring'); const isDraftBeingDeleted = this.id.startsWith('deleted-'); return isReminder || isDraftBeingDeleted; diff --git a/app/src/flux/modules/reflux-coffee.coffee b/app/src/flux/modules/reflux-coffee.coffee index 135c0b398..acb1a749d 100644 --- a/app/src/flux/modules/reflux-coffee.coffee +++ b/app/src/flux/modules/reflux-coffee.coffee @@ -103,7 +103,7 @@ module.exports = setupEmitter: -> return if @_emitter @_emitter ?= new EventEmitter() - @_emitter.setMaxListeners(50) + @_emitter.setMaxListeners(100) listen: (callback, bindContext) -> if not callback diff --git a/app/src/flux/stores/draft-factory.coffee b/app/src/flux/stores/draft-factory.coffee index 6ac1b2ee4..e0cbc8a32 100644 --- a/app/src/flux/stores/draft-factory.coffee +++ b/app/src/flux/stores/draft-factory.coffee @@ -158,6 +158,33 @@ class DraftFactory """ ) + createDraftForResurfacing: (thread, threadMessageId, body) => + account = AccountStore.accountForId(thread.accountId) + if threadMessageId + rthmsid = Promise.resolve(threadMessageId) + else + rthmsid = DatabaseStore + .findBy(Message, {accountId: thread.accountId, threadId: thread.id}) + .order(Message.attributes.date.descending()) + .limit(1) + .then((msg) => + return (msg && msg.headerMessageId || "") + ) + + return rthmsid.then((replyToHeaderMessageId) => + @createDraft({ + from: [new Contact({ email: account.emailAddress, name: "#{account.name} via Mailspring" })], + to: [account.defaultMe()], + cc: [], + pristine: false, + subject: thread.subject, + threadId: thread.id, + accountId: thread.accountId, + replyToHeaderMessageId: replyToHeaderMessageId, + body: body + }) + ) + candidateDraftForUpdating: (message, behavior) => if behavior not in ['prefer-existing-if-pristine', 'prefer-existing'] return Promise.resolve(null) diff --git a/app/src/flux/stores/draft-store.es6 b/app/src/flux/stores/draft-store.es6 index b5fa5e948..b6d791d62 100644 --- a/app/src/flux/stores/draft-store.es6 +++ b/app/src/flux/stores/draft-store.es6 @@ -146,6 +146,9 @@ class DraftStore extends MailspringStore { }; _onSendQuickReply = ({ thread, threadId, message, messageId }, body) => { + if (AppEnv.config.get('core.sending.sounds')) { + SoundRegistry.playSound('hit-send'); + } return Promise.props(this._modelifyContext({ thread, threadId, message, messageId })) .then(({ message: m, thread: t }) => { return DraftFactory.createDraftForReply({ message: m, thread: t, type: 'reply' }); @@ -346,10 +349,6 @@ class DraftStore extends MailspringStore { throw new Error(`Cant find send action ${sendActionKey} `); } - if (AppEnv.config.get('core.sending.sounds')) { - SoundRegistry.playSound('hit-send'); - } - // get the draft session, apply any last-minute edits and get the final draft. // We need to call `changes.commit` here to ensure the body of the draft is // completely saved and the user won't see old content briefly. diff --git a/app/src/flux/tasks/send-draft-task.es6 b/app/src/flux/tasks/send-draft-task.es6 index 5567bdb67..97e0c5401 100644 --- a/app/src/flux/tasks/send-draft-task.es6 +++ b/app/src/flux/tasks/send-draft-task.es6 @@ -67,7 +67,7 @@ export default class SendDraftTask extends Task { } label() { - return 'Sending message'; + return this.silent ? null : 'Sending message'; } validate() {