diff options
Diffstat (limited to 'web-ui/public/js/mail_view/ui')
18 files changed, 2021 insertions, 0 deletions
diff --git a/web-ui/public/js/mail_view/ui/attachment_icon.js b/web-ui/public/js/mail_view/ui/attachment_icon.js new file mode 100644 index 00000000..e04fc02a --- /dev/null +++ b/web-ui/public/js/mail_view/ui/attachment_icon.js @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'page/events', + 'features' + ], + + function (defineComponent, events, features) { + 'use strict'; + + return defineComponent(function () { + this.render = function () { + this.$node.html('<i class="fa fa-paperclip"></i>'); + }; + + this.triggerUploadAttachment = function () { + this.trigger(document, events.mail.startUploadAttachment); + }; + + this.uploadInProgress = function (ev, data) { + this.attr.busy = true; + this.$node.addClass('busy'); + }; + + this.uploadFinished = function (ev, data) { + this.attr.busy = false; + this.$node.removeClass('busy'); + }; + + this.after('initialize', function () { + if (features.isEnabled('attachment')) { + this.render(); + this.on(document, events.mail.uploadingAttachment, this.uploadInProgress); + this.on(document, events.mail.uploadedAttachment, this.uploadFinished); + this.on(document, events.mail.failedUploadAttachment, this.uploadFinished); + } + this.on(this.$node, 'click', function() { + if (!this.attr.busy) { + this.triggerUploadAttachment(); + } + }); + }); + }); + }); diff --git a/web-ui/public/js/mail_view/ui/attachment_list.js b/web-ui/public/js/mail_view/ui/attachment_list.js new file mode 100644 index 00000000..4ef64960 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/attachment_list.js @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2015 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'views/templates', + 'page/events', + 'helpers/view_helper', + 'helpers/monitored_ajax' + ], + + function (templates, events, viewHelper, monitoredAjax) { + 'use strict'; + + function attachmentList() { + this.defaultAttrs({ + inputFileUpload: '#fileupload', + attachmentListItem: '#attachment-list-item', + attachmentUploadItem: '#attachment-upload-item', + attachmentUploadItemProgress: '#attachment-upload-item-progress', + attachmentUploadItemAbort: '#attachment-upload-item-abort', + attachmentBaseUrl: '/attachment', + attachments: [], + closeIcon: '#upload-error-close', + uploadError: '#upload-error', + dismissButton: '#dismiss-button', + uploadFileButton: '#upload-file-button' + }); + + var ONE_MEGABYTE = 1024*1024; + var ATTACHMENT_SIZE_LIMIT = 5*ONE_MEGABYTE; + + this.showAttachment = function (ev, data) { + this.trigger(document, events.mail.appendAttachment, data); + this.renderAttachmentListView(data); + }; + + this.addAttachment = function (event, data) { + this.attr.attachments.push(data); + }; + + this.renderAttachmentListView = function (data) { + var currentHtml = this.select('attachmentListItem').html(); + var item = this.buildAttachmentListItem(data); + this.select('attachmentListItem').append(item); + }; + + this.buildAttachmentListItem = function (attachment) { + var attachmentData = {ident: attachment.ident, + encoding: attachment.encoding, + name: attachment.name, + size: attachment.size, + removable: true}; + + var element = $(templates.compose.attachmentItem(attachmentData)); + var self = this; + element.find('i.remove-icon').bind('click', function(event) { + var element = $(this); + var ident = element.closest('li').attr('data-ident'); + self.trigger(document, events.mail.removeAttachment, {ident: ident, element: element}); + event.preventDefault(); + }); + return element; + }; + + this.performPreUploadCheck = function(e, data) { + if (data.originalFiles[0].size > ATTACHMENT_SIZE_LIMIT) { + return false; + } + + return true; + }; + + this.removeUploadError = function() { + var uploadError = this.select('uploadError'); + if (uploadError) { + uploadError.remove(); + } + }; + + this.showUploadError = function () { + var self = this; + + var html = $(templates.compose.uploadAttachmentFailed()); + html.insertAfter(self.select('attachmentListItem')); + + self.on(self.select('closeIcon'), 'click', dismissUploadFailed); + self.on(self.select('dismissButton'), 'click', dismissUploadFailed); + self.on(self.select('uploadFileButton'), 'click', uploadAnotherFile); + + function dismissUploadFailed(event) { + event.preventDefault(); + self.select('uploadError').remove(); + } + + function uploadAnotherFile(event) { + event.preventDefault(); + self.trigger(document, events.mail.startUploadAttachment); + } + }; + + this.showUploadProgressBar = function(e, data) { + var element = $(templates.compose.attachmentUploadItem({ + name: data.originalFiles[0].name, + size: data.originalFiles[0].size + })); + this.select('attachmentUploadItem').append(element); + this.select('attachmentUploadItem').show(); + }; + + this.hideUploadProgressBar = function() { + this.select('attachmentUploadItem').hide(); + this.select('attachmentUploadItem').empty(); + }; + + this.attachUploadAbort = function(e, data) { + this.on(this.select('attachmentUploadItemAbort'), 'click', function(e) { + data.abort(); + e.preventDefault(); + }); + }; + + this.detachUploadAbort = function() { + this.off(this.select('attachmentUploadItemAbort'), 'click'); + }; + + this.addJqueryFileUploadConfig = function() { + var self = this; + + self.removeUploadError(); + + this.select('inputFileUpload').fileupload({ + add: function(e, data) { + if (self.performPreUploadCheck(e, data)) { + self.showUploadProgressBar(e, data); + self.attachUploadAbort(e, data); + data.submit(); + } else { + self.showUploadError(); + } + }, + url: self.attr.attachmentBaseUrl, + dataType: 'json', + done: function (e, response) { + self.detachUploadAbort(); + self.hideUploadProgressBar(); + self.trigger(document, events.mail.uploadedAttachment, response.result); + }, + fail: function(e, data){ + self.detachUploadAbort(); + self.hideUploadProgressBar(); + self.trigger(document, events.mail.failedUploadAttachment); + }, + progressall: function (e, data) { + var progressRate = parseInt(data.loaded / data.total * 100, 10); + self.select('attachmentUploadItemProgress').css('width', progressRate + '%'); + } + }).bind('fileuploadstart', function (e) { + self.trigger(document, events.mail.uploadingAttachment); + }); + }; + + this.startUpload = function () { + this.addJqueryFileUploadConfig(); + this.select('inputFileUpload').click(); + }; + + this.removeAttachmentFromList = function(ident) { + for (var i = 0; i < this.attr.attachments.length; i++) { + if (this.attr.attachments[i].ident === ident) { + this.attr.attachments.remove(i); + break; + } + } + }; + + this.destroyAttachmentElement = function(element) { + element.closest('li').remove(); + }; + + this.removeAttachments = function(event, data) { + this.removeAttachmentFromList(data.ident); + this.destroyAttachmentElement(data.element); + }; + + this.after('initialize', function () { + this.addJqueryFileUploadConfig(); + this.on(document, events.mail.uploadedAttachment, this.showAttachment); + this.on(document, events.mail.startUploadAttachment, this.startUpload); + this.on(document, events.mail.appendAttachment, this.addAttachment); + this.on(document, events.mail.removeAttachment, this.removeAttachments); + }); + } + + return attachmentList; + }); diff --git a/web-ui/public/js/mail_view/ui/compose_box.js b/web-ui/public/js/mail_view/ui/compose_box.js new file mode 100644 index 00000000..101dc939 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/compose_box.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_mail_edit_base', + 'page/events', + 'mail_view/data/mail_builder' + ], + + function (defineComponent, templates, withMailEditBase, events, mailBuilder) { + 'use strict'; + + return defineComponent(composeBox, withMailEditBase); + + function composeBox() { + + this.defaultAttrs({ + 'closeButton': '.close-mail-button' + }); + + this.showNoMessageSelected = function() { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.buildMail = function(tag) { + return this.builtMail(tag).build(); + }; + + this.builtMail = function(tag) { + return mailBuilder.newMail(this.attr.ident) + .subject(this.select('subjectBox').val()) + .to(this.attr.recipientValues.to) + .cc(this.attr.recipientValues.cc) + .bcc(this.attr.recipientValues.bcc) + .body(this.select('bodyBox').val()) + .attachment(this.attr.attachments) + .tag(tag); + }; + + this.renderComposeBox = function() { + this.render(templates.compose.box, {}); + this.enableFloatlabel('input.floatlabel'); + this.enableFloatlabel('textarea.floatlabel'); + this.select('recipientsFields').show(); + this.on(this.select('closeButton'), 'click', this.showNoMessageSelected); + this.enableAutoSave(); + }; + + this.mailDeleted = function(event, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + } + }; + + this.discardDraft = function () { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.after('initialize', function () { + this.renderComposeBox(); + + this.select('toBox').focus(); + this.on(document, events.mail.deleted, this.mailDeleted); + this.on(document, events.mail.sent, this.showNoMessageSelected); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/draft_box.js b/web-ui/public/js/mail_view/ui/draft_box.js new file mode 100644 index 00000000..afe31914 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/draft_box.js @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_mail_edit_base', + 'page/events', + 'mail_view/data/mail_builder' + ], + + function (defineComponent, templates, withMailEditBase, events, mailBuilder) { + 'use strict'; + + return defineComponent(draftBox, withMailEditBase); + + function draftBox() { + this.defaultAttrs({ + closeMailButton: '.close-mail-button' + }); + + this.showNoMessageSelected = function() { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.buildMail = function(tag) { + return this.builtMail(tag).build(); + }; + + this.builtMail = function(tag) { + return mailBuilder.newMail(this.attr.ident) + .subject(this.select('subjectBox').val()) + .to(this.attr.recipientValues.to) + .cc(this.attr.recipientValues.cc) + .bcc(this.attr.recipientValues.bcc) + .body(this.select('bodyBox').val()) + .attachment(this.attr.attachments) + .tag(tag); + }; + + this.renderDraftBox = function(ev, data) { + var mail = data.mail; + var body = mail.textPlainBody; + this.attr.ident = mail.ident; + this.render(templates.compose.box, { + recipients: { + to: mail.header.to, + cc: mail.header.cc, + bcc: mail.header.bcc + }, + subject: mail.header.subject, + body: body, + attachments: this.convertToRemovableAttachments(mail.attachments) + }); + + var self = this; + this.$node.find('i.remove-icon').bind('click', function(event) { + var element = $(this); + var ident = element.closest('li').attr('data-ident'); + self.trigger(document, events.mail.removeAttachment, {ident: ident, element: element}); + event.preventDefault(); + }); + + this.enableFloatlabel('input.floatlabel'); + this.enableFloatlabel('textarea.floatlabel'); + this.select('recipientsFields').show(); + this.select('bodyBox').focus(); + this.select('tipMsg').hide(); + this.enableAutoSave(); + this.bindCollapse(); + this.on(this.select('closeMailButton'), 'click', this.showNoMessageSelected); + }; + + this.convertToRemovableAttachments = function(attachments) { + return attachments.map(function(attachment) { + attachment.removable = true; + return attachment; + }); + }; + + this.mailDeleted = function(event, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + } + }; + + this.after('initialize', function () { + this.on(this, events.mail.here, this.renderDraftBox); + this.on(document, events.mail.sent, this.showNoMessageSelected); + this.on(document, events.mail.deleted, this.mailDeleted); + this.trigger(document, events.mail.want, { mail: this.attr.mailIdent, caller: this }); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/draft_save_status.js b/web-ui/public/js/mail_view/ui/draft_save_status.js new file mode 100644 index 00000000..47751d91 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/draft_save_status.js @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, events, i18n) { + 'use strict'; + + return defineComponent(draftSaveStatus); + + function draftSaveStatus() { + this.setMessage = function(msg) { + var node = this.$node; + return function () { node.text(msg); }; + }; + + this.after('initialize', function () { + this.on(document, events.mail.saveDraft, this.setMessage(i18n.t('draft-saving'))); + this.on(document, events.mail.draftSaved, this.setMessage(i18n.t('draft-saved'))); + this.on(document, events.ui.mail.changedSinceLastSave, this.setMessage('')); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/feedback_box.js b/web-ui/public/js/mail_view/ui/feedback_box.js new file mode 100644 index 00000000..4e00ece8 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/feedback_box.js @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define(['flight/lib/component', 'views/templates', 'page/events', 'features', 'feedback/feedback_cache'], + function (defineComponent, templates, events, features, feedbackCache) { + 'use strict'; + + return defineComponent(function () { + this.defaultAttrs({ + 'closeButton': '.close-mail-button', + 'submitButton': '#send-button', + 'textBox': '#text-box', + }); + + this.render = function () { + this.$node.html(templates.compose.feedback()); + }; + + this.startCachingData = function () { + this.select('textBox').val(feedbackCache.getCache()); + this.select('textBox').on('change', this.cacheFeedbackData.bind(this)); + }; + + + this.cacheFeedbackData = function () { + feedbackCache.setCache(this.select('textBox').val()); + }; + + this.showNoMessageSelected = function () { + this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.submitFeedback = function () { + var feedback = this.select('textBox').val(); + this.trigger(document, events.feedback.submit, {feedback: feedback}); + feedbackCache.resetCache(); + }; + + this.showSuccessMessage = function () { + this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Thanks for your feedback!'}); + }; + + this.after('initialize', function () { + if (features.isEnabled('feedback')) { + this.render(); + this.startCachingData(); + this.on(document, events.feedback.submitted, this.showNoMessageSelected); + this.on(document, events.feedback.submitted, this.showSuccessMessage); + this.on(this.select('closeButton'), 'click', this.showNoMessageSelected); + this.on(this.select('submitButton'), 'click', this.submitFeedback); + } + }); + + }); + }); diff --git a/web-ui/public/js/mail_view/ui/forward_box.js b/web-ui/public/js/mail_view/ui/forward_box.js new file mode 100644 index 00000000..a34bd55d --- /dev/null +++ b/web-ui/public/js/mail_view/ui/forward_box.js @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_compose_inline', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) { + 'use strict'; + + return defineComponent(forwardBox, withHideAndShow, withComposeInline); + + function forwardBox() { + var fwd = function(v) { return i18n.t('fwd') + ': ' + v; }; + + this.fetchTargetMail = function (ev) { + this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this }); + }; + + this.setupForwardBox = function() { + var mail = this.attr.mail; + this.attr.subject = fwd(mail.header.subject); + this.attr.attachments = mail.attachments; + + this.renderInlineCompose('forward-box', { + subject: this.attr.subject, + recipients: { to: [], cc: []}, + body: viewHelper.quoteMail(mail), + attachments: this.convertToRemovableAttachments(mail.attachments) + }); + + var self = this; + this.$node.find('i.remove-icon').bind('click', function(event) { + var element = $(this); + var ident = element.closest('li').attr('data-ident'); + self.trigger(document, events.mail.removeAttachment, {ident: ident}); + event.preventDefault(); + }); + + this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput); + this.select('recipientsDisplay').hide(); + this.select('recipientsFields').show(); + }; + + this.convertToRemovableAttachments = function(attachments) { + return attachments.map(function(attachment) { + attachment.removable = true; + return attachment; + }); + }; + + this.showSubjectInput = function() { + this.select('subjectDisplay').hide(); + this.select('subjectInput').show(); + this.select('subjectInput').focus(); + }; + + this.buildMail = function(tag) { + var builder = this.builtMail(tag).subject(this.select('subjectInput').val()); + + var headersToFwd = ['bcc', 'cc', 'date', 'from', 'message_id', 'reply_to', 'sender', 'to']; + var header = this.attr.mail.header; + _.each(headersToFwd, function (h) { + if (!_.isUndefined(header[h])) { + builder.header('resent_' + h, header[h]); + } + }); + + return builder.build(); + }; + + this.after('initialize', function () { + this.setupForwardBox(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/mail_actions.js b/web-ui/public/js/mail_view/ui/mail_actions.js new file mode 100644 index 00000000..65cd0aaa --- /dev/null +++ b/web-ui/public/js/mail_view/ui/mail_actions.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events' + ], + + function (defineComponent, templates, events) { + 'use strict'; + + return defineComponent(mailActions); + + function mailActions() { + + this.defaultAttrs({ + replyButtonTop: '#reply-button-top', + viewMoreActions: '#view-more-actions', + replyAllButtonTop: '#reply-all-button-top', + deleteButtonTop: '#delete-button-top', + moreActions: '#more-actions' + }); + + + this.displayMailActions = function () { + + this.$node.html(templates.mails.mailActions()); + + this.select('moreActions').hide(); + + this.on(this.select('replyButtonTop'), 'click', function () { + this.trigger(document, events.ui.replyBox.showReply); + }.bind(this)); + + this.on(this.select('replyAllButtonTop'), 'click', function () { + this.trigger(document, events.ui.replyBox.showReplyAll); + this.select('moreActions').hide(); + }.bind(this)); + + this.on(this.select('deleteButtonTop'), 'click', function () { + this.trigger(document, events.ui.mail.delete, {mail: this.attr.mail}); + this.select('moreActions').hide(); + }.bind(this)); + + this.on(this.select('viewMoreActions'), 'click', function () { + this.select('moreActions').toggle(); + }.bind(this)); + + this.on(this.select('viewMoreActions'), 'blur', function (event) { + var replyButtonTopHover = this.select('replyAllButtonTop').is(':hover'); + var deleteButtonTopHover = this.select('deleteButtonTop').is(':hover'); + + if (replyButtonTopHover || deleteButtonTopHover) { + event.preventDefault(); + } else { + this.select('moreActions').hide(); + } + }.bind(this)); + + }; + + this.after('initialize', function () { + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.displayMailActions(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/mail_view.js b/web-ui/public/js/mail_view/ui/mail_view.js new file mode 100644 index 00000000..3408c8af --- /dev/null +++ b/web-ui/public/js/mail_view/ui/mail_view.js @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'mail_view/ui/mail_actions', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_mail_tagging', + 'mixins/with_mail_sandbox', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, templates, mailActions, viewHelpers, withHideAndShow, withMailTagging, withMailSandbox, events, i18n) { + 'use strict'; + + return defineComponent(mailView, mailActions, withHideAndShow, withMailTagging, withMailSandbox); + + function mailView() { + this.defaultAttrs({ + tags: '.mail-read-view__header-tags-tag', + newTagInput: '#new-tag-input', + newTagButton: '#new-tag-button', + addNew: '.mail-read-view__header-tags-new-button', + trashButton: '#trash-button', + archiveButton: '#archive-button', + closeMailButton: '.close-mail-button' + }); + + this.displayMail = function (event, data) { + this.attr.mail = data.mail; + + var signed, encrypted, attachments; + + data.mail.security_casing = data.mail.security_casing || {}; + signed = this.checkSigned(data.mail); + encrypted = this.checkEncrypted(data.mail); + attachments = data.mail.attachments.map(function (attachment) { + attachment.received = true; + return attachment; + }); + + if(data.mail.mailbox === 'sent') { + encrypted = undefined; + signed = undefined; + } + + this.$node.html(templates.mails.fullView({ + header: data.mail.header, + body: [], + statuses: viewHelpers.formatStatusClasses(data.mail.status), + ident: data.mail.ident, + tags: data.mail.tags, + encryptionStatus: encrypted, + signatureStatus: signed, + attachments: attachments + })); + + this.showMailOnSandbox(this.attr.mail); + + this.attachTagCompletion(this.attr.mail); + + this.select('tags').on('click', function (event) { + this.removeTag($(event.target).text()); + }.bind(this)); + + this.addTagLoseFocus(); + this.on(this.select('newTagButton'), 'click', this.showNewTagInput); + this.on(this.select('newTagInput'), 'keydown', this.handleKeyDown); + this.on(this.select('newTagInput'), 'blur', this.addTagLoseFocus); + this.on(this.select('trashButton'), 'click', this.moveToTrash); + this.on(this.select('closeMailButton'), 'click', this.openNoMessageSelectedPane); + + mailActions.attachTo('#mail-actions', data); + this.resetScroll(); + }; + + this.resetScroll = function(){ + $('#right-pane').scrollTop(0); + }; + + this.checkEncrypted = function(mail) { + if(_.isEmpty(mail.security_casing.locks)) { + return { + cssClass: 'security-status__label--not-encrypted', + label: 'not-encrypted' + }; + } + + var statusClass = ['security-status__label--encrypted']; + var statusLabel; + + var hasAnyEncryptionInfo = _.any(mail.security_casing.locks, function (lock) { + return lock.state === 'valid'; + }); + + if(hasAnyEncryptionInfo) { + statusLabel = 'encrypted'; + } else { + statusClass.push('--with-error'); + statusLabel = 'encryption-error'; + } + + return { + cssClass: statusClass.join(''), + label: statusLabel + }; + }; + + this.checkSigned = function(mail) { + var statusNotSigned = { + cssClass: 'security-status__label--not-signed', + label: 'not-signed' + }; + + if(_.isEmpty(mail.security_casing.imprints)) { + return statusNotSigned; + } + + var hasNoSignatureInformation = _.any(mail.security_casing.imprints, function (imprint) { + return imprint.state === 'no_signature_information'; + }); + + if(hasNoSignatureInformation) { + return statusNotSigned; + } + + var statusClass = ['security-status__label--signed']; + var statusLabel = ['signed']; + + if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_revoked'; })) { + statusClass.push('--revoked'); + statusLabel.push('signature-revoked'); + } + + if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_expired'; })) { + statusClass.push('--expired'); + statusLabel.push('signature-expired'); + } + + if(this.isNotTrusted(mail)) { + statusClass.push('--not-trusted'); + statusLabel.push('signature-not-trusted'); + } + + return { + cssClass: statusClass.join(''), + label: statusLabel.join(' ') + }; + }; + + this.isNotTrusted = function(mail){ + return _.any(mail.security_casing.imprints, function(imprint) { + if(_.isNull(imprint.seal)){ + return true; + } + var currentTrust = _.isUndefined(imprint.seal.trust) ? imprint.seal.validity : imprint.seal.trust; + return currentTrust === 'no_trust'; + }); + }; + + this.openNoMessageSelectedPane = function(ev, data) { + this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.handleKeyDown = function(event) { + var ENTER_KEY = 13; + var ESC_KEY = 27; + + if (event.which === ENTER_KEY){ + event.preventDefault(); + if (this.select('newTagInput').val().trim() !== '') { + this.createNewTag(); + } + } else if (event.which === ESC_KEY) { + event.preventDefault(); + this.addTagLoseFocus(); + } + }; + + this.addTagLoseFocus = function () { + this.select('newTagInput').hide(); + this.select('newTagInput').typeahead('val', ''); + this.select('addNew').show(); + }; + + this.showNewTagInput = function () { + this.select('newTagInput').show(); + this.select('newTagInput').focus(); + this.select('addNew').hide(); + }; + + this.removeTag = function (tag) { + tag = tag.toString(); + var filteredTags = _.without(this.attr.mail.tags, tag); + this.updateTags(this.attr.mail, filteredTags); + this.trigger(document, events.dispatchers.tags.refreshTagList); + }; + + this.moveToTrash = function(){ + this.trigger(document, events.ui.mail.delete, { mail: this.attr.mail }); + }; + + this.tagsUpdated = function(ev, data) { + data = data || {}; + this.attr.mail.tags = data.tags; + this.displayMail({}, { mail: this.attr.mail }); + }; + + this.mailDeleted = function(ev, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.mail.ident)) { + this.openNoMessageSelectedPane(); + } + }; + + this.fetchMailToShow = function () { + this.trigger(events.mail.want, {mail: this.attr.ident, caller: this}); + }; + + this.highlightMailContent = function (event, data) { + // we can't directly manipulate the iFrame to highlight the content + // so we need to take an indirection where we directly manipulate + // the mail content to accomodate the highlighting + this.trigger(document, events.mail.highlightMailContent, data); + }; + + this.after('initialize', function () { + this.on(this, events.mail.notFound, this.openNoMessageSelectedPane); + this.on(this, events.mail.here, this.highlightMailContent); + this.on(document, events.mail.display, this.displayMail); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.on(document, events.mail.tags.updated, this.tagsUpdated); + this.on(document, events.mail.deleted, this.mailDeleted); + this.fetchMailToShow(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/no_mails_available_pane.js b/web-ui/public/js/mail_view/ui/no_mails_available_pane.js new file mode 100644 index 00000000..c62c6b30 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/no_mails_available_pane.js @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_hide_and_show', + 'page/events' + ], + + function(defineComponent, templates, withHideAndShow, events) { + 'use strict'; + + //return defineComponent(noMailsAvailablePane, withHideAndShow); + return defineComponent(noMailsAvailablePane); + + function noMailsAvailablePane() { + this.defaultAttrs({ + tag: null, + forSearch: '' + }); + + var mailsQueryMatch = /-?in:"?[\w]+"?|tag:"[\w]+"/g; + + this.render = function() { + this.attr.tag = 'tags.' + this.attr.tag; + this.attr.forSearch = this.attr.forSearch.replace(mailsQueryMatch, '').trim(); + this.$node.html(templates.noMailsAvailable(this.attr)); + }; + + this.after('initialize', function () { + this.render(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/no_message_selected_pane.js b/web-ui/public/js/mail_view/ui/no_message_selected_pane.js new file mode 100644 index 00000000..a5fc2393 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/no_message_selected_pane.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_hide_and_show', + 'page/events' + ], + + function(defineComponent, templates, withHideAndShow, events) { + 'use strict'; + + return defineComponent(noMessageSelectedPane, withHideAndShow); + + function noMessageSelectedPane() { + this.render = function() { + this.$node.html(templates.noMessageSelected()); + }; + + this.after('initialize', function () { + this.render(); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipient.js b/web-ui/public/js/mail_view/ui/recipients/recipient.js new file mode 100644 index 00000000..c13a52b1 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipient.js @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events' + ], + + function (defineComponent, templates, events) { + 'use strict'; + + return defineComponent(recipient); + + function recipient() { + this.renderAndPrepend = function (nodeToPrependTo, recipient) { + var html = $(templates.compose.fixedRecipient(recipient)); + html.insertBefore(nodeToPrependTo.children().last()); + var component = new this.constructor(); + component.initialize(html, recipient); + component.attr.recipient = recipient; + return component; + }; + + this.recipientDelActions = function () { + this.on(this.$node.find('.recipient-del'), 'click', function (event) { + this.doSelect(); + this.trigger(events.ui.recipients.deleteRecipient, this); + event.preventDefault(); + }); + + this.on(this.$node.find('.recipient-del'), 'mouseover', function () { + this.$node.find('.recipient-value').addClass('deleting'); + this.$node.find('.recipient-del').addClass('deleteTooltip'); + }); + + this.on(this.$node.find('.recipient-del'), 'mouseout', function () { + this.$node.find('.recipient-value').removeClass('deleting'); + this.$node.find('.recipient-del').removeClass('deleteTooltip'); + }); + }; + + this.destroy = function () { + this.$node.remove(); + this.teardown(); + }; + + this.doSelect = function () { + this.$node.find('.recipient-value').addClass('selected'); + }; + + this.doUnselect = function () { + this.$node.find('.recipient-value').removeClass('selected'); + }; + + this.isSelected = function () { + return this.$node.find('.recipient-value').hasClass('selected'); + }; + + this.sinalizeInvalid = function () { + this.$node.find('.recipient-value>span').addClass('invalid-format'); + }; + + this.discoverEncryption = function () { + this.$node.addClass('discover-encryption'); + var p = $.getJSON('/keys?search=' + this.attr.address).promise(); + p.done(function () { + this.$node.find('.recipient-value').addClass('encrypted'); + this.$node.removeClass('discover-encryption'); + }.bind(this)); + p.fail(function () { + this.$node.find('.recipient-value').addClass('not-encrypted'); + this.$node.removeClass('discover-encryption'); + }.bind(this)); + }; + + this.getMailAddress = function() { + return this.$node.find('input[type=hidden]').val(); + }; + + this.triggerEditRecipient = function(event, element) { + this.trigger(this.$node.closest('.recipients-area'), events.ui.recipients.clickToEdit, this); + }; + + this.after('initialize', function () { + this.recipientDelActions(); + this.on('click', this.triggerEditRecipient); + + if (this.attr.invalidAddress){ + this.sinalizeInvalid(); + } else { + this.discoverEncryption(); + } + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients.js b/web-ui/public/js/mail_view/ui/recipients/recipients.js new file mode 100644 index 00000000..2caa8d14 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients.js @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events', + 'helpers/iterator', + 'mail_view/ui/recipients/recipients_input', + 'mail_view/ui/recipients/recipient', + 'mail_view/ui/recipients/recipients_iterator' + ], + function (defineComponent, templates, events, Iterator, RecipientsInput, Recipient, RecipientsIterator) { + 'use strict'; + + return defineComponent(recipients); + + function recipients() { + this.defaultAttrs({ + navigationHandler: '.recipients-navigation-handler', + recipientsList: '.recipients-list' + }); + + function getAddresses(recipients) { + return _.flatten(_.map(recipients, function (e) { return e.attr.address;})); + } + + function moveLeft() { this.attr.iterator.moveLeft(); } + function moveRight() { this.attr.iterator.moveRight(); } + function deleteCurrentRecipient() { + this.attr.iterator.deleteCurrent(); + this.addressesUpdated(); + } + + function editCurrentRecipient(event, recipient) { + var mailAddr = this.attr.iterator.current().getMailAddress(); + this.attr.iterator.deleteCurrent(); + this.attr.input.$node.val(mailAddr).focus(); + this.unselectAllRecipients(); + this.addressesUpdated(); + } + + this.clickToEditRecipient = function(event, recipient) { + this.attr.iterator = null; + var mailAddr = recipient.getMailAddress(); + + var position = this.getRecipientPosition(recipient); + this.attr.recipients.splice(position, 1); + recipient.destroy(); + + this.addressesUpdated(); + this.unselectAllRecipients(); + this.attr.input.$node.val(mailAddr).focus(); + }; + + this.getRecipientPosition = function(recipient) { + return recipient.$node.closest('.recipients-area').find('.fixed-recipient').index(recipient.$node); + }; + + this.unselectAllRecipients = function() { + this.$node.find('.recipient-value.selected').removeClass('selected'); + }; + + var SPECIAL_KEYS_ACTIONS = { + 8: deleteCurrentRecipient, + 46: deleteCurrentRecipient, + 32: editCurrentRecipient, + 13: editCurrentRecipient, + 37: moveLeft, + 39: moveRight + }; + + this.addRecipient = function(recipient) { + var newRecipient = Recipient.prototype.renderAndPrepend(this.$node.find(this.attr.recipientsList), recipient); + this.attr.recipients.push(newRecipient); + }; + + this.recipientEntered = function (event, recipient) { + this.addRecipient(recipient); + this.addressesUpdated(); + }; + + this.invalidRecipientEntered = function(event, recipient) { + recipient.invalidAddress = true; + this.addRecipient(recipient); + }; + + this.deleteRecipient = function (event, recipient) { + this.attr.iterator = null; + var position = this.getRecipientPosition(recipient); + + this.attr.recipients.splice(position, 1); + recipient.destroy(); + + this.addressesUpdated(); + }; + + this.deleteLastRecipient = function () { + this.attr.recipients.pop().destroy(); + this.addressesUpdated(); + }; + + this.enterNavigationMode = function () { + this.attr.iterator = new RecipientsIterator({ + elements: this.attr.recipients, + exitInput: this.attr.input.$node + }); + + this.attr.iterator.current().doSelect(); + this.attr.input.$node.blur(); + this.select('navigationHandler').focus(); + }; + + this.leaveNavigationMode = function () { + if(this.attr.iterator) { this.attr.iterator.current().unselect(); } + this.attr.iterator = null; + }; + + this.selectLastRecipient = function () { + if (this.attr.recipients.length === 0) { return; } + this.enterNavigationMode(); + }; + + this.attachInput = function () { + this.attr.input = RecipientsInput.prototype.attachAndReturn(this.$node.find('input[type=text]'), this.attr.name); + }; + + this.processSpecialKey = function (event) { + if(SPECIAL_KEYS_ACTIONS.hasOwnProperty(event.which)) { SPECIAL_KEYS_ACTIONS[event.which].apply(this); } + }; + + this.initializeAddresses = function () { + _.each(_.flatten(this.attr.addresses), function (address) { + this.addRecipient({ address: address, name: this.attr.name }); + }.bind(this)); + }; + + this.addressesUpdated = function() { + this.trigger(document, events.ui.recipients.updated, {recipientsName: this.attr.name, newRecipients: getAddresses(this.attr.recipients)}); + }; + + this.doCompleteRecipients = function () { + var address = this.attr.input.$node.val(); + if (!_.isEmpty(address)) { + var recipient = Recipient.prototype.renderAndPrepend(this.$node, { name: this.attr.name, address: address }); + this.attr.recipients.push(recipient); + this.attr.input.$node.val(''); + } + + this.trigger(document, events.ui.recipients.updated, { + recipientsName: this.attr.name, + newRecipients: getAddresses(this.attr.recipients), + skipSaveDraft: true + }); + + }; + + this.after('initialize', function () { + this.attr.recipients = []; + this.attachInput(); + this.initializeAddresses(); + + this.on(events.ui.recipients.deleteRecipient, this.deleteRecipient); + this.on(events.ui.recipients.deleteLast, this.deleteLastRecipient); + this.on(events.ui.recipients.selectLast, this.selectLastRecipient); + this.on(events.ui.recipients.entered, this.recipientEntered); + this.on(events.ui.recipients.enteredInvalid, this.invalidRecipientEntered); + this.on(events.ui.recipients.clickToEdit, this.clickToEditRecipient); + + this.on(document, events.ui.recipients.doCompleteInput, this.doCompleteRecipients); + + this.on(this.attr.input.$node, 'focus', this.leaveNavigationMode); + this.on(this.select('navigationHandler'), 'keydown', this.processSpecialKey); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + }); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_input.js b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js new file mode 100644 index 00000000..8a9c4eaf --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define([ + 'flight/lib/component', + 'page/events', + 'features' + ], + function (defineComponent, events, features) { + 'use strict'; + + function recipientsInput() { + var EXIT_KEY_CODE_MAP = { + 8: 'backspace', + 37: 'left' + }, + ENTER_ADDRESS_KEY_CODE_MAP = { + 9: 'tab', + 186: 'semicolon', + 188: 'comma', + 13: 'enter', + 27: 'esc' + }, + EVENT_FOR = { + 8: events.ui.recipients.deleteLast, + 37: events.ui.recipients.selectLast + }, + self; + + var simpleAddressMatch = /[^<\w,;]?([^\s<;,]+@[\w-]+\.[^\s>;,]+)/; + var canonicalAddressMatch = /([^,;\s][^,;@]+<[^\s;,]+@[\w-]+\.[^\s;,]+>)/; + var emailAddressMatch = new RegExp([simpleAddressMatch.source, '|', canonicalAddressMatch.source].join(''), 'g'); + + var extractContactNames = function (response) { + return _.map(response, function(a) { return { value: a }; }); + }; + + function createEmailCompleter() { + var emailCompleter = new Bloodhound({ + datumTokenizer: function (d) { + return [d.value]; + }, + queryTokenizer: function (q) { + return [q.trim()]; + }, + remote: { + url: '/contacts?q=%QUERY', + filter: extractContactNames + } + }); + emailCompleter.initialize(); + return emailCompleter; + } + + function reset(node) { + node.typeahead('val', ''); + } + + function caretIsInTheBeginningOfInput(input) { + return input.selectionStart === 0; + } + + function isExitKey(keyPressed) { + return EXIT_KEY_CODE_MAP.hasOwnProperty(keyPressed); + } + + function isEnterAddressKey(keyPressed) { + return ENTER_ADDRESS_KEY_CODE_MAP.hasOwnProperty(keyPressed); + } + + this.processSpecialKey = function (event) { + var keyPressed = event.which; + + if (isExitKey(keyPressed) && caretIsInTheBeginningOfInput(this.$node[0])) { + this.trigger(EVENT_FOR[keyPressed]); + return; + } + + if (!event.shiftKey && isEnterAddressKey(keyPressed)) { + this.tokenizeRecipient(event); + + if ((keyPressed !== 9 /* tab */)) { + event.preventDefault(); + } + } + + }; + + this.tokenizeRecipient = function (event) { + if (_.isEmpty(this.$node.val().trim())) { + return; + } + + this.recipientSelected(null, {value: this.$node.val() }); + event.preventDefault(); + }; + + this.recipientSelected = function (event, data) { + var value = (data && data.value) || this.$node.val(); + + var validAddresses = this.extractValidAddresses(value); + var invalidAddresses = this.extractInvalidAddresses(value); + + this.triggerEventForEach(validAddresses, events.ui.recipients.entered); + this.triggerEventForEach(invalidAddresses, events.ui.recipients.enteredInvalid); + + reset(this.$node); + }; + + this.triggerEventForEach = function (addresses, event) { + var that = this; + _.each(addresses, function(address) { + if (!_.isEmpty(address.trim())) { + that.trigger(that.$node, event, { name: that.attr.name, address: address.trim() }); + } + }); + }; + + this.extractValidAddresses = function(rawAddresses) { + return rawAddresses.match(emailAddressMatch); + }; + + this.extractInvalidAddresses = function(rawAddresses) { + return rawAddresses.replace(emailAddressMatch, '').split(/[,;]/); + }; + + this.init = function () { + this.$node.typeahead({ + hint: true, + highlight: true, + minLength: 1 + }, { + source: createEmailCompleter().ttAdapter(), + templates: { + suggestion: function (o) { return _.escape(o.value); } + } + }); + }; + + this.attachAndReturn = function (node, name) { + var input = new this.constructor(); + input.initialize(node, { name: name}); + return input; + }; + + this.warnSendButtonOfInputState = function () { + var toTrigger = _.isEmpty(this.$node.val()) ? events.ui.recipients.inputFieldIsEmpty : events.ui.recipients.inputFieldHasCharacters; + this.trigger(document, toTrigger, { name: this.attr.name }); + }; + + this.after('initialize', function () { + self = this; + this.init(); + this.on('typeahead:selected typeahead:autocompleted', this.recipientSelected); + this.on(this.$node, 'focusout', this.tokenizeRecipient); + this.on(this.$node, 'keydown', this.processSpecialKey); + this.on(this.$node, 'keyup', this.warnSendButtonOfInputState); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + + return defineComponent(recipientsInput); + + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js new file mode 100644 index 00000000..624ac4f5 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define(['helpers/iterator'], function (Iterator) { + 'use strict'; + + return RecipientsIterator; + + function RecipientsIterator(options) { + + this.iterator = new Iterator(options.elements, options.elements.length - 1); + this.input = options.exitInput; + + this.current = function () { + return this.iterator.current(); + }; + + this.moveLeft = function () { + if (this.iterator.hasPrevious()) { + this.iterator.current().doUnselect(); + this.iterator.previous().doSelect(); + } + }; + + this.moveRight = function () { + this.iterator.current().doUnselect(); + if (this.iterator.hasNext()) { + this.iterator.next().doSelect(); + } else { + this.input.focus(); + } + }; + + this.deleteCurrent = function () { + this.iterator.removeCurrent().destroy(); + + if (this.iterator.hasElements()) { + this.iterator.current().doSelect(); + } else { + this.input.focus(); + } + }; + } + +}); diff --git a/web-ui/public/js/mail_view/ui/reply_box.js b/web-ui/public/js/mail_view/ui/reply_box.js new file mode 100644 index 00000000..a174d185 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/reply_box.js @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_compose_inline', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) { + 'use strict'; + + return defineComponent(replyBox, withHideAndShow, withComposeInline); + + function replyBox() { + this.defaultAttrs({ + replyType: 'reply', + draftReply: false, + mail: null, + mailBeingRepliedIdent: undefined + }); + + this.getRecipients = function() { + if (this.attr.replyType === 'replyall') { + return this.attr.mail.replyToAllAddress(); + } else { + return this.attr.mail.replyToAddress(); + } + }; + + var re = function(v) { return i18n.t('re') + ': ' + v; }; + + this.setupReplyBox = function() { + var recipients, body; + + if (this.attr.draftReply){ + this.attr.ident = this.attr.mail.ident; + this.attr.mailBeingRepliedIdent = this.attr.mail.draft_reply_for; + + recipients = this.attr.mail.recipients(); + body = this.attr.mail.body; + this.attr.subject = this.attr.mail.header.subject; + } else { + this.attr.mailBeingRepliedIdent = this.attr.mail.ident; + recipients = this.getRecipients(); + body = viewHelper.quoteMail(this.attr.mail); + this.attr.subject = re(this.attr.mail.header.subject); + } + + this.attr.recipientValues.to = recipients.to; + this.attr.recipientValues.cc = recipients.cc; + + this.renderInlineCompose('reply-box', { + recipients: recipients, + subject: this.attr.subject, + body: body + }); + + this.on(this.select('recipientsDisplay'), 'click keydown', this.showRecipientFields); + this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput); + }; + + this.showRecipientFields = function(ev, data) { + if(!ev.keyCode || ev.keyCode === 13){ + this.select('recipientsDisplay').hide(); + this.select('recipientsFields').show(); + $('#recipients-to-area .tt-input').focus(); + } + }; + + this.showSubjectInput = function() { + this.select('subjectDisplay').hide(); + this.select('subjectInput').show(); + this.select('subjectInput').focus(); + }; + + this.buildMail = function(tag) { + var builder = this.builtMail(tag).subject(this.select('subjectInput').val()); + if(!_.isUndefined(this.attr.mail.header.message_id)) { + builder.header('in_reply_to', this.attr.mail.header.message_id); + } + + if(!_.isUndefined(this.attr.mail.header.list_id)) { + builder.header('list_id', this.attr.mail.header.list_id); + } + + var mail = builder.build(); + mail.setDraftReplyFor(this.attr.mailBeingRepliedIdent); + + return mail; + }; + + this.after('initialize', function () { + this.setupReplyBox(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/reply_section.js b/web-ui/public/js/mail_view/ui/reply_section.js new file mode 100644 index 00000000..cbe64205 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/reply_section.js @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mail_view/ui/reply_box', + 'mail_view/ui/forward_box', + 'mixins/with_hide_and_show', + 'mixins/with_feature_toggle', + 'page/events' + ], + + function (defineComponent, templates, ReplyBox, ForwardBox, withHideAndShow, withFeatureToggle, events) { + 'use strict'; + + return defineComponent(replySection, withHideAndShow, withFeatureToggle('replySection')); + + function replySection() { + this.defaultAttrs({ + replyButton: '#reply-button', + replyAllButton: '#reply-all-button', + forwardButton: '#forward-button', + replyBox: '#reply-box', + replyType: 'reply', + replyContainer: '.reply-container' + }); + + this.showReply = function() { + this.attr.replyType = 'reply'; + this.fetchEmailToReplyTo(); + }; + + this.showReplyAll = function() { + this.attr.replyType = 'replyall'; + this.fetchEmailToReplyTo(); + }; + + this.showForward = function() { + this.attr.replyType = 'forward'; + this.fetchEmailToReplyTo(); + }; + + this.render = function () { + this.$node.html(templates.compose.replySection); + + this.on(this.select('replyButton'), 'click', this.showReply); + this.on(this.select('replyAllButton'), 'click', this.showReplyAll); + this.on(this.select('forwardButton'), 'click', this.showForward); + }; + + this.checkForDraftReply = function() { + this.render(); + this.hideContainer(); + + this.trigger(document, events.mail.draftReply.want, {ident: this.attr.ident}); + }; + + this.fetchEmailToReplyTo = function (ev) { + this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this }); + }; + + this.showDraftReply = function(ev, data) { + this.showContainer(); + this.hideButtons(); + ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, draftReply: true }); + }; + + this.showReplyComposeBox = function (ev, data) { + this.showContainer(); + this.hideButtons(); + if(this.attr.replyType === 'forward') { + ForwardBox.attachTo(this.select('replyBox'), { mail: data.mail }); + } else { + ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, replyType: this.attr.replyType }); + } + }; + + this.hideContainer = function() { + this.select('replyContainer').hide(); + }; + + this.showContainer = function() { + this.select('replyContainer').show(); + }; + + this.hideButtons = function() { + this.select('replyButton').hide(); + this.select('replyAllButton').hide(); + this.select('forwardButton').hide(); + }; + + this.showButtons = function () { + this.showContainer(); + this.select('replyBox').empty(); + this.select('replyButton').show(); + this.select('replyAllButton').show(); + this.select('forwardButton').show(); + }; + + this.after('initialize', function () { + this.on(document, events.ui.replyBox.showReply, this.showReply); + this.on(document, events.ui.replyBox.showReplyAll, this.showReplyAll); + this.on(document, events.ui.composeBox.trashReply, this.showButtons); + this.on(this, events.mail.here, this.showReplyComposeBox); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + + this.on(document, events.ui.replyBox.showReplyContainer, this.showContainer); + this.on(document, events.mail.draftReply.here, this.showDraftReply); + + this.checkForDraftReply(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/send_button.js b/web-ui/public/js/mail_view/ui/send_button.js new file mode 100644 index 00000000..66fe1233 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/send_button.js @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define([ + 'flight/lib/component', + 'flight/lib/utils', + 'page/events', + 'helpers/view_helper' + ], + function (defineComponent, utils, events, viewHelper) { + 'use strict'; + + return defineComponent(sendButton); + + function sendButton() { + var RECIPIENTS_BOXES_COUNT = 3; + + this.enableButton = function () { + this.$node.prop('disabled', false); + }; + + this.disableButton = function () { + this.$node.prop('disabled', true); + }; + + this.atLeastOneInputFieldHasRecipients = function () { + return _.any(_.values(this.attr.recipients), function (e) { return !_.isEmpty(e); }); + }; + + this.atLeastOneInputFieldHasCharacters = function () { + return _.any(_.values(this.attr.inputFieldHasCharacters), function (e) { return e === true; }); + }; + + this.updateButton = function () { + if (this.attr.sendingInProgress === false) { + if (this.attr.uploading === false && (this.atLeastOneInputFieldHasCharacters() || this.atLeastOneInputFieldHasRecipients())) { + this.enableButton(); + } else { + this.disableButton(); + } + } + }; + + this.inputFieldIsEmpty = function (ev, data) { + this.attr.inputFieldHasCharacters[data.name] = false; + this.updateButton(); + }; + + this.inputFieldHasCharacters = function (ev, data) { + this.attr.inputFieldHasCharacters[data.name] = true; + this.updateButton(); + }; + + this.uploadInProgress = function (ev, data) { + this.attr.uploading = true; + this.updateButton(); + }; + + this.uploadFinished = function (ev, data) { + this.attr.uploading = false; + this.updateButton(); + }; + + this.updateRecipientsForField = function (ev, data) { + this.attr.recipients[data.recipientsName] = data.newRecipients; + this.attr.inputFieldHasCharacters[data.recipientsName] = false; + + this.updateButton(); + }; + + this.updateRecipientsAndSendMail = function () { + + this.on(document, events.ui.mail.recipientsUpdated, utils.countThen(RECIPIENTS_BOXES_COUNT, function () { + this.trigger(document, events.ui.mail.send); + this.off(document, events.ui.mail.recipientsUpdated); + }.bind(this))); + + this.disableButton(); + this.$node.text(viewHelper.i18n.t('sending-mail')); + + this.attr.sendingInProgress = true; + + this.trigger(document, events.ui.recipients.doCompleteInput); + }; + + this.resetButton = function () { + this.attr.sendingInProgress = false; + this.attr.uploading = false; + this.$node.html(viewHelper.i18n.t('send')); + this.enableButton(); + }; + + this.after('initialize', function () { + this.attr.recipients = {}; + this.attr.inputFieldHasCharacters = {}; + this.resetButton(); + + this.on(document, events.ui.recipients.inputFieldHasCharacters, this.inputFieldHasCharacters); + this.on(document, events.ui.recipients.inputFieldIsEmpty, this.inputFieldIsEmpty); + this.on(document, events.ui.recipients.updated, this.updateRecipientsForField); + + this.on(this.$node, 'click', this.updateRecipientsAndSendMail); + + this.on(document, events.mail.uploadingAttachment, this.uploadInProgress); + this.on(document, events.mail.uploadedAttachment, this.uploadFinished); + this.on(document, events.mail.failedUploadAttachment, this.uploadFinished); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.on(document, events.ui.sendbutton.enable, this.resetButton); + this.on(document, events.mail.send_failed, this.resetButton); + + this.disableButton(); + }); + } + + } +); |