summaryrefslogtreecommitdiff
path: root/web-ui/app/js/mail_view
diff options
context:
space:
mode:
authorOla Bini <ola.bini@gmail.com>2014-07-31 19:29:33 -0300
committerOla Bini <ola.bini@gmail.com>2014-07-31 19:29:33 -0300
commit04cf441c5ae18400c6b4865b0b37a71718dc9d46 (patch)
treedd0b0d049ec00389e2d4561b226c46eb1682b997 /web-ui/app/js/mail_view
parent639a663a4c37020003586438fdcd7ac529a00f10 (diff)
Add web-ui based on previous code
Diffstat (limited to 'web-ui/app/js/mail_view')
-rw-r--r--web-ui/app/js/mail_view/data/mail_builder.js79
-rw-r--r--web-ui/app/js/mail_view/data/mail_sender.js74
-rw-r--r--web-ui/app/js/mail_view/ui/compose_box.js63
-rw-r--r--web-ui/app/js/mail_view/ui/draft_box.js74
-rw-r--r--web-ui/app/js/mail_view/ui/draft_save_status.js25
-rw-r--r--web-ui/app/js/mail_view/ui/forward_box.js66
-rw-r--r--web-ui/app/js/mail_view/ui/mail_actions.js70
-rw-r--r--web-ui/app/js/mail_view/ui/mail_view.js242
-rw-r--r--web-ui/app/js/mail_view/ui/no_message_selected_pane.js25
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipient.js36
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipients.js127
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipients_input.js140
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js41
-rw-r--r--web-ui/app/js/mail_view/ui/reply_box.js102
-rw-r--r--web-ui/app/js/mail_view/ui/reply_section.js102
-rw-r--r--web-ui/app/js/mail_view/ui/send_button.js85
16 files changed, 1351 insertions, 0 deletions
diff --git a/web-ui/app/js/mail_view/data/mail_builder.js b/web-ui/app/js/mail_view/data/mail_builder.js
new file mode 100644
index 00000000..27820c1a
--- /dev/null
+++ b/web-ui/app/js/mail_view/data/mail_builder.js
@@ -0,0 +1,79 @@
+/*global _ */
+
+define(['services/model/mail'], function (mailModel) {
+ 'use strict';
+
+ var mail;
+
+ function recipients(mail, place, v) {
+ if (v !== '' && !_.isUndefined(v)) {
+ if(_.isArray(v)) {
+ mail[place] = v;
+ } else {
+ mail[place] = v.split(' ');
+ }
+ } else {
+ mail[place] = [];
+ }
+ }
+
+ return {
+ newMail: function(ident) {
+ ident = _.isUndefined(ident) ? '' : ident;
+
+ mail = {
+ header: {
+ to: [],
+ cc: [],
+ bcc: [],
+ from: undefined,
+ subject: ''
+ },
+ tags: [],
+ body: '',
+ ident: ident
+ };
+ return this;
+ },
+
+ subject: function (subject) {
+ mail.header.subject = subject;
+ return this;
+ },
+
+ body: function(body) {
+ mail.body = body;
+ return this;
+ },
+
+ to: function (to) {
+ recipients(mail.header, 'to', to);
+ return this;
+ },
+
+ cc: function (cc) {
+ recipients(mail.header, 'cc', cc);
+ return this;
+ },
+
+ bcc: function (bcc) {
+ recipients(mail.header, 'bcc', bcc);
+ return this;
+ },
+
+ header: function(name, value) {
+ mail.header[name] = value;
+ return this;
+ },
+
+ tag: function(tag) {
+ if(_.isUndefined(tag)) { tag = 'drafts'; }
+ mail.tags.push(tag);
+ return this;
+ },
+
+ build: function() {
+ return mailModel.create(mail);
+ }
+ };
+});
diff --git a/web-ui/app/js/mail_view/data/mail_sender.js b/web-ui/app/js/mail_view/data/mail_sender.js
new file mode 100644
index 00000000..7440f5a7
--- /dev/null
+++ b/web-ui/app/js/mail_view/data/mail_sender.js
@@ -0,0 +1,74 @@
+define(
+ [
+ 'flight/lib/component',
+ 'mail_view/data/mail_builder',
+ 'page/events'
+ ],
+ function (defineComponent, mailBuilder, events) {
+ 'use strict';
+
+ return defineComponent(mailSender);
+
+ function mailSender() {
+ function successSendMail(on){
+ return function(result) {
+ on.trigger(document, events.mail.sent, result);
+ };
+ }
+
+ function successSaveDraft(on){
+ return function(result){
+ on.trigger(document, events.mail.draftSaved, result);
+ };
+ }
+
+ function failure(on) {
+ return function(xhr, status, error) {
+ on.trigger(events.ui.userAlerts.displayMessage, {message: 'Ops! something went wrong, try again later.'});
+ };
+ }
+
+ this.defaultAttrs({
+ mailsResource: '/mails'
+ });
+
+ this.sendMail = function(event, data) {
+ $.ajax(this.attr.mailsResource, {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data)
+ }).done(successSendMail(this))
+ .fail(failure(this));
+ };
+
+ this.saveMail = function(mail) {
+ var method = (mail.ident === '') ? 'POST' : 'PUT';
+
+ return $.ajax(this.attr.mailsResource, {
+ type: method,
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(mail)
+ });
+ };
+
+ this.saveDraft = function(event, data) {
+ this.saveMail(data)
+ .done(successSaveDraft(this))
+ .fail(failure(this));
+ };
+
+ this.saveMailWithCallback = function(event, data) {
+ this.saveMail(data.mail)
+ .done(function(result) { return data.callback(result); })
+ .fail(function(result) { return data.callback(result); });
+ };
+
+ this.after('initialize', function () {
+ this.on(events.mail.send, this.sendMail);
+ this.on(events.mail.saveDraft, this.saveDraft);
+ this.on(document, events.mail.save, this.saveMailWithCallback);
+ });
+ }
+ });
diff --git a/web-ui/app/js/mail_view/ui/compose_box.js b/web-ui/app/js/mail_view/ui/compose_box.js
new file mode 100644
index 00000000..41954192
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/compose_box.js
@@ -0,0 +1,63 @@
+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())
+ .tag(tag);
+ };
+
+ this.renderComposeBox = function() {
+ this.render(templates.compose.box, {});
+ 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.after('initialize', function () {
+ this.renderComposeBox();
+
+ this.select('subjectBox').focus();
+ this.on(this.select('cancelButton'), 'click', this.showNoMessageSelected);
+ this.on(document, events.mail.deleted, this.mailDeleted);
+
+ this.on(document, events.mail.sent, this.showNoMessageSelected);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_view/ui/draft_box.js b/web-ui/app/js/mail_view/ui/draft_box.js
new file mode 100644
index 00000000..95cffd14
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/draft_box.js
@@ -0,0 +1,74 @@
+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())
+ .tag(tag);
+ };
+
+ this.renderDraftBox = function(ev, data) {
+ var mail = data.mail;
+ 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: mail.body
+ });
+
+ this.select('recipientsFields').show();
+ this.select('bodyBox').focus();
+ this.select('tipMsg').hide();
+ this.enableAutoSave();
+ this.on(this.select('cancelButton'), 'click', this.showNoMessageSelected);
+ this.on(this.select('closeMailButton'), 'click', this.showNoMessageSelected);
+ };
+
+ 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/app/js/mail_view/ui/draft_save_status.js b/web-ui/app/js/mail_view/ui/draft_save_status.js
new file mode 100644
index 00000000..f56f82f1
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/draft_save_status.js
@@ -0,0 +1,25 @@
+define(
+ [
+ 'flight/lib/component',
+ 'page/events'
+ ],
+
+ function (defineComponent, events) {
+ '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('Saving to Drafts...'));
+ this.on(document, events.mail.draftSaved, this.setMessage('Draft Saved.'));
+ this.on(document, events.ui.mail.changedSinceLastSave, this.setMessage(''));
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_view/ui/forward_box.js b/web-ui/app/js/mail_view/ui/forward_box.js
new file mode 100644
index 00000000..e112b43d
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/forward_box.js
@@ -0,0 +1,66 @@
+/*global Smail */
+/*global _ */
+
+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('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.renderInlineCompose('forward-box', {
+ subject: this.attr.subject,
+ recipients: { to: [], cc: []},
+ body: viewHelper.quoteMail(mail)
+ });
+
+ this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput);
+ this.select('recipientsDisplay').hide();
+ this.select('recipientsFields').show();
+ };
+
+ 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/app/js/mail_view/ui/mail_actions.js b/web-ui/app/js/mail_view/ui/mail_actions.js
new file mode 100644
index 00000000..dc16ea9f
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/mail_actions.js
@@ -0,0 +1,70 @@
+/*global Smail */
+/*global _ */
+
+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/app/js/mail_view/ui/mail_view.js b/web-ui/app/js/mail_view/ui/mail_view.js
new file mode 100644
index 00000000..1e27c879
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/mail_view.js
@@ -0,0 +1,242 @@
+/*global Smail */
+/*global _ */
+/*global Bloodhound */
+
+'use strict';
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mail_view/ui/mail_actions',
+ 'helpers/view_helper',
+ 'mixins/with_hide_and_show',
+ 'page/events',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, templates, mailActions, viewHelpers, withHideAndShow, events, i18n) {
+
+ return defineComponent(mailView, mailActions, withHideAndShow);
+
+ function mailView() {
+ this.defaultAttrs({
+ tags: '.tag',
+ newTagInput: '#new-tag-input',
+ newTagButton: '#new-tag-button',
+ addNew: '.add-new',
+ deleteModal: '#delete-modal',
+ trashButton: '#trash-button',
+ archiveButton: '#archive-button',
+ closeModalButton: '.close-reveal-modal',
+ closeMailButton: '.close-mail-button'
+ });
+
+ this.attachTagCompletion = function() {
+ this.attr.tagCompleter = new Bloodhound({
+ datumTokenizer: function(d) { return [d.value]; },
+ queryTokenizer: function(q) { return [q.trim()]; },
+ remote: {
+ url: '/tags?q=%QUERY',
+ filter: function(pr) { return _.map(pr, function(pp) { return {value: pp.name}; }); }
+ }
+ });
+
+ this.attr.tagCompleter.initialize();
+
+ this.select('newTagInput').typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 1
+ }, {
+ source: this.attr.tagCompleter.ttAdapter()
+ });
+ };
+
+ this.displayMail = function (event, data) {
+ this.attr.mail = data.mail;
+
+ var date = new Date(data.mail.header.date);
+ data.mail.header.formattedDate = viewHelpers.getFormattedDate(date);
+
+ data.mail.security_casing = data.mail.security_casing || {};
+ var signed = this.checkSigned(data.mail);
+ var encrypted = this.checkEncrypted(data.mail);
+
+ 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
+ }));
+
+ this.$node.find('.bodyArea').html(viewHelpers.formatMailBody(data.mail));
+ this.trigger(document, events.search.highlightResults, {where: '.bodyArea'});
+ this.trigger(document, events.search.highlightResults, {where: '.subjectArea'});
+ this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'});
+
+ this.attachTagCompletion();
+
+ this.select('tags').on('click', function (event) {
+ this.removeTag($(event.target).data('tag'));
+ }.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'), 'typeahead:selected typeahead:autocompleted', this.createNewTag.bind(this));
+ this.on(this.select('newTagInput'), 'blur', this.addTagLoseFocus);
+ this.on(this.select('trashButton'), 'click', this.moveToTrash);
+ this.on(this.select('archiveButton'), 'click', this.archiveIt);
+ this.on(this.select('closeModalButton'), 'click', this.closeModal);
+ 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 'not-encrypted'; }
+
+ var status = ['encrypted'];
+
+ if(_.any(mail.security_casing.locks, function (lock) { return lock.state === 'valid'; })) { status.push('encryption-valid'); }
+ else { status.push('encryption-failure'); }
+
+ return status.join(' ');
+ };
+
+ this.checkSigned = function(mail) {
+ if(_.isEmpty(mail.security_casing.imprints)) { return 'not-signed'; }
+
+ var status = ['signed'];
+
+ if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_revoked'; })) {
+ status.push('signature-revoked');
+ }
+ if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_expired'; })) {
+ status.push('signature-expired');
+ }
+
+ if(this.isNotTrusted(mail)) {
+ status.push('signature-not-trusted');
+ }
+
+
+ return status.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.createNewTag = function() {
+ var tagsCopy = this.attr.mail.tags.slice();
+ tagsCopy.push(this.select('newTagInput').val());
+ this.attr.tagCompleter.clear();
+ this.attr.tagCompleter.clearPrefetchCache();
+ this.attr.tagCompleter.clearRemoteCache();
+ this.trigger(document, events.mail.tags.update, { ident: this.attr.mail.ident, tags: _.uniq(tagsCopy)});
+ this.trigger(document, events.dispatchers.tags.refreshTagList);
+ };
+
+ this.handleKeyDown = function(event) {
+ var ENTER_KEY = 13;
+ var ESC_KEY = 27;
+
+ if (event.which === ENTER_KEY){
+ event.preventDefault();
+ 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) {
+ var filteredTags = _.without(this.attr.mail.tags, tag);
+ if (_.isEmpty(filteredTags)){
+ this.displayMail({}, { mail: this.attr.mail });
+ this.select('deleteModal').foundation('reveal', 'open');
+ } else {
+ this.updateTags(filteredTags);
+ }
+ };
+
+ this.moveToTrash = function(){
+ this.closeModal();
+ this.trigger(document, events.ui.mail.delete, { mail: this.attr.mail });
+ };
+
+ this.archiveIt = function() {
+ this.updateTags([]);
+ this.closeModal();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.get('Your message was archive it!') });
+ this.openNoMessageSelectedPane();
+ };
+
+ this.closeModal = function() {
+ $('#delete-modal').foundation('reveal', 'close');
+ };
+
+ this.updateTags = function(tags) {
+ this.trigger(document, events.mail.tags.update, {ident: this.attr.mail.ident, tags: tags});
+ };
+
+ this.tagsUpdated = function(ev, data) {
+ data = data || {};
+ this.attr.mail.tags = data.tags;
+ this.displayMail({}, { mail: this.attr.mail });
+ this.trigger(document, events.ui.tagList.refresh);
+ };
+
+ 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.after('initialize', function () {
+ this.on(this, events.mail.here, this.displayMail);
+ this.on(this, events.mail.notFound, this.openNoMessageSelectedPane);
+ 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/app/js/mail_view/ui/no_message_selected_pane.js b/web-ui/app/js/mail_view/ui/no_message_selected_pane.js
new file mode 100644
index 00000000..64b9a8ff
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/no_message_selected_pane.js
@@ -0,0 +1,25 @@
+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/app/js/mail_view/ui/recipients/recipient.js b/web-ui/app/js/mail_view/ui/recipients/recipient.js
new file mode 100644
index 00000000..967ff61b
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/recipients/recipient.js
@@ -0,0 +1,36 @@
+'use strict';
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates'
+ ],
+
+ function (defineComponent, templates) {
+
+ 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);
+ return component;
+ };
+
+ this.destroy = function () {
+ this.$node.remove();
+ this.teardown();
+ };
+
+ this.select = function () {
+ this.$node.find('.recipient-value').addClass('selected');
+ };
+
+ this.unselect = function () {
+ this.$node.find('.recipient-value').removeClass('selected');
+ };
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_view/ui/recipients/recipients.js b/web-ui/app/js/mail_view/ui/recipients/recipients.js
new file mode 100644
index 00000000..86f9b9d3
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/recipients/recipients.js
@@ -0,0 +1,127 @@
+/*global _ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events',
+ 'mail_view/ui/recipients/recipients_input',
+ 'mail_view/ui/recipients/recipient',
+ 'mail_view/ui/recipients/recipients_iterator'
+ ],
+ function (defineComponent, templates, events, RecipientsInput, Recipient, RecipientsIterator) {
+ 'use strict';
+
+ return defineComponent(recipients);
+
+ function recipients() {
+ this.defaultAttrs({
+ navigationHandler: '.recipients-navigation-handler'
+ });
+
+ 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();
+ }
+
+ var SPECIAL_KEYS_ACTIONS = {
+ 8: deleteCurrentRecipient,
+ 46: deleteCurrentRecipient,
+ 37: moveLeft,
+ 39: moveRight
+ };
+
+ this.addRecipient = function(recipient) {
+ var newRecipient = Recipient.prototype.renderAndPrepend(this.$node, recipient);
+ this.attr.recipients.push(newRecipient);
+ };
+
+ this.recipientEntered = function (event, recipient) {
+ this.addRecipient(recipient);
+ 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().select();
+ 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.deleteLast, this.deleteLastRecipient);
+ this.on(events.ui.recipients.selectLast, this.selectLastRecipient);
+ this.on(events.ui.recipients.entered, this.recipientEntered);
+
+ 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/app/js/mail_view/ui/recipients/recipients_input.js b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js
new file mode 100644
index 00000000..79780ad2
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js
@@ -0,0 +1,140 @@
+/*global _*/
+/*global Bloodhound */
+'use strict';
+
+define([
+ 'flight/lib/component',
+ 'page/events'
+ ],
+ function (defineComponent, events) {
+
+ function recipientsInput() {
+ var EXIT_KEY_CODE_MAP = {
+ 8: 'backspace',
+ 37: 'left'
+ },
+ ENTER_ADDRESS_KEY_CODE_MAP = {
+ 9: 'tab',
+ 186: 'semicolon',
+ 188: 'comma',
+ 32: 'space',
+ 13: 'enter',
+ 27: 'esc'
+ },
+ EVENT_FOR = {
+ 8: events.ui.recipients.deleteLast,
+ 37: events.ui.recipients.selectLast
+ },
+ self;
+
+ var extractContactNames = function (response) {
+ return _.flatten(response.contacts, function (contact) {
+ var filterCriteria = contact.name ?
+ function (e) {
+ return { value: contact.name + ' <' + e + '>' };
+ } :
+ function (e) {
+ return { value: e };
+ };
+
+ return _.map(contact.addresses, filterCriteria);
+ });
+ };
+
+ 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 (isEnterAddressKey(keyPressed)) {
+ if (!_.isEmpty(this.$node.val())) {
+ this.recipientSelected(null, { value: this.$node.val() });
+ event.preventDefault();
+ }
+ if((keyPressed !== 9 /* tab */)) {
+ event.preventDefault();
+ }
+ }
+
+ };
+
+ this.recipientSelected = function (event, data) {
+ var value = (data && data.value) || this.$node.val();
+
+ this.trigger(this.$node, events.ui.recipients.entered, { name: this.attr.name, address: value });
+ reset(this.$node);
+ };
+
+ this.init = function () {
+ this.$node.typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 1
+ }, {
+ source: createEmailCompleter().ttAdapter()
+ });
+ };
+
+ 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.inputHasNoMail : events.ui.recipients.inputHasMail;
+ 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, '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/app/js/mail_view/ui/recipients/recipients_iterator.js b/web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js
new file mode 100644
index 00000000..73aefc79
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js
@@ -0,0 +1,41 @@
+define(['helpers/iterator'], function (Iterator) {
+
+ 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().unselect();
+ this.iterator.previous().select();
+ }
+ };
+
+ this.moveRight = function () {
+ this.iterator.current().unselect();
+ if (this.iterator.hasNext()) {
+ this.iterator.next().select();
+ } else {
+ this.input.focus();
+ }
+ };
+
+ this.deleteCurrent = function () {
+ this.iterator.removeCurrent().destroy();
+
+ if (this.iterator.hasElements()) {
+ this.iterator.current().select()
+ } else {
+ this.input.focus();
+ }
+ };
+ }
+
+}); \ No newline at end of file
diff --git a/web-ui/app/js/mail_view/ui/reply_box.js b/web-ui/app/js/mail_view/ui/reply_box.js
new file mode 100644
index 00000000..2c39d8d6
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/reply_box.js
@@ -0,0 +1,102 @@
+/*global Smail */
+/*global _ */
+
+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('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/app/js/mail_view/ui/reply_section.js b/web-ui/app/js/mail_view/ui/reply_section.js
new file mode 100644
index 00000000..866a255a
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/reply_section.js
@@ -0,0 +1,102 @@
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mail_view/ui/reply_box',
+ 'mail_view/ui/forward_box',
+ 'mixins/with_hide_and_show',
+ 'page/events'
+ ],
+
+ function (defineComponent, templates, ReplyBox, ForwardBox, withHideAndShow, events) {
+ 'use strict';
+
+ return defineComponent(replySection, withHideAndShow);
+
+ function replySection() {
+ this.defaultAttrs({
+ replyButton: '#reply-button',
+ replyAllButton: '#reply-all-button',
+ forwardButton: '#forward-button',
+ replyBox: '#reply-box',
+ replyType: 'reply'
+ });
+
+ 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.select('replyButton').hide();
+ this.select('replyAllButton').hide();
+ this.select('forwardButton').hide();
+
+ 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.hideButtons();
+ ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, draftReply: true });
+ };
+
+ this.showReplyComposeBox = function (ev, data) {
+ 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.hideButtons = function() {
+ this.select('replyButton').hide();
+ this.select('replyAllButton').hide();
+ this.select('forwardButton').hide();
+ };
+
+ this.showButtons = function () {
+ 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.mail.draftReply.notFound, this.showButtons);
+ this.on(document, events.mail.draftReply.here, this.showDraftReply);
+
+ this.checkForDraftReply();
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_view/ui/send_button.js b/web-ui/app/js/mail_view/ui/send_button.js
new file mode 100644
index 00000000..44df3ae6
--- /dev/null
+++ b/web-ui/app/js/mail_view/ui/send_button.js
@@ -0,0 +1,85 @@
+/*global _ */
+'use strict';
+
+define([
+ 'flight/lib/component',
+ 'flight/lib/utils',
+ 'page/events'
+ ],
+ function (defineComponent, utils, events) {
+
+ 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.atLeastOneFieldHasRecipients = function () {
+ return _.any(_.values(this.attr.recipients), function (e) { return !_.isEmpty(e); });
+ };
+
+ this.atLeastOneInputHasMail = function () {
+ return _.any(_.values(this.attr.inputHasMail), function (e) { return e === true; });
+ };
+
+ this.updateButton = function () {
+ if (this.atLeastOneInputHasMail() || this.atLeastOneFieldHasRecipients()) {
+ this.enableButton();
+ } else {
+ this.disableButton();
+ }
+ };
+
+ this.inputHasNoMail = function (ev, data) {
+ this.attr.inputHasMail[data.name] = false;
+ this.updateButton();
+ };
+
+ this.inputHasMail = function (ev, data) {
+ this.attr.inputHasMail[data.name] = true;
+ this.updateButton();
+ };
+
+ this.updateRecipientsForField = function (ev, data) {
+ this.attr.recipients[data.recipientsName] = data.newRecipients;
+ this.attr.inputHasMail[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.trigger(document, events.ui.recipients.doCompleteInput);
+ };
+
+ this.after('initialize', function () {
+ this.attr.recipients = {};
+ this.attr.inputHasMail = {};
+
+ this.on(document, events.ui.recipients.inputHasMail, this.inputHasMail);
+ this.on(document, events.ui.recipients.inputHasNoMail, this.inputHasNoMail);
+ this.on(document, events.ui.recipients.updated, this.updateRecipientsForField);
+
+ this.on(this.$node, 'click', this.updateRecipientsAndSendMail);
+
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ this.on(document, events.ui.sendbutton.enable, this.enableButton);
+
+ this.disableButton();
+ });
+ }
+
+ }
+);