summaryrefslogtreecommitdiff
path: root/web-ui/app/js
diff options
context:
space:
mode:
authorRoald de Vries <rdevries@thoughtworks.com>2016-12-08 16:59:09 +0100
committerRoald de Vries <rdevries@thoughtworks.com>2016-12-08 16:59:09 +0100
commitfafac3b4128a0993b0de1c6e8ca3062bf1ccc14e (patch)
tree3b9a446e4c82bb8ba94c1cd0adec57c0042dae28 /web-ui/app/js
parent521bce7eff5cf921156efe74c91a0499ade43619 (diff)
Revert "[#801] Merge branch 'signup'"
This reverts commit d10f607a4d40587510b0dc31b31fe4750bf4a3a3, reversing changes made to c28abba2f5b1186c671ebef508d40ffaae6d5bc5.
Diffstat (limited to 'web-ui/app/js')
-rw-r--r--web-ui/app/js/dispatchers/left_pane_dispatcher.js62
-rw-r--r--web-ui/app/js/dispatchers/middle_pane_dispatcher.js74
-rw-r--r--web-ui/app/js/dispatchers/right_pane_dispatcher.js117
-rw-r--r--web-ui/app/js/features/features.js61
-rw-r--r--web-ui/app/js/feedback/feedback_cache.js35
-rw-r--r--web-ui/app/js/feedback/feedback_trigger.js39
-rw-r--r--web-ui/app/js/foundation/initialize_foundation.js5
-rw-r--r--web-ui/app/js/foundation/off_canvas.js46
-rw-r--r--web-ui/app/js/helpers/browser.js36
-rw-r--r--web-ui/app/js/helpers/contenttype.js184
-rw-r--r--web-ui/app/js/helpers/iterator.js60
-rw-r--r--web-ui/app/js/helpers/monitored_ajax.js67
-rw-r--r--web-ui/app/js/helpers/sanitizer.js126
-rw-r--r--web-ui/app/js/helpers/triggering.js29
-rw-r--r--web-ui/app/js/helpers/view_helper.js163
-rw-r--r--web-ui/app/js/lib/highlightRegex.js127
-rw-r--r--web-ui/app/js/lib/html4-defs.js640
-rw-r--r--web-ui/app/js/mail_list/domain/refresher.js43
-rw-r--r--web-ui/app/js/mail_list/ui/mail_item_factory.js59
-rw-r--r--web-ui/app/js/mail_list/ui/mail_items/draft_item.js55
-rw-r--r--web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.js97
-rw-r--r--web-ui/app/js/mail_list/ui/mail_items/mail_item.js88
-rw-r--r--web-ui/app/js/mail_list/ui/mail_items/sent_item.js61
-rw-r--r--web-ui/app/js/mail_list/ui/mail_list.js182
-rw-r--r--web-ui/app/js/mail_list_actions/ui/archive_many_trigger.js29
-rw-r--r--web-ui/app/js/mail_list_actions/ui/compose_trigger.js57
-rw-r--r--web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js47
-rw-r--r--web-ui/app/js/mail_list_actions/ui/mail_list_actions.js93
-rw-r--r--web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js47
-rw-r--r--web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js47
-rw-r--r--web-ui/app/js/mail_list_actions/ui/pagination_trigger.js66
-rw-r--r--web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js47
-rw-r--r--web-ui/app/js/mail_list_actions/ui/refresh_trigger.js44
-rw-r--r--web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js49
-rw-r--r--web-ui/app/js/mail_view/data/feedback_sender.js49
-rw-r--r--web-ui/app/js/mail_view/data/mail_builder.js102
-rw-r--r--web-ui/app/js/mail_view/data/mail_sender.js93
-rw-r--r--web-ui/app/js/mail_view/ui/attachment_icon.js61
-rw-r--r--web-ui/app/js/mail_view/ui/attachment_list.js210
-rw-r--r--web-ui/app/js/mail_view/ui/compose_box.js84
-rw-r--r--web-ui/app/js/mail_view/ui/draft_box.js109
-rw-r--r--web-ui/app/js/mail_view/ui/draft_save_status.js42
-rw-r--r--web-ui/app/js/mail_view/ui/feedback_box.js69
-rw-r--r--web-ui/app/js/mail_view/ui/forward_box.js97
-rw-r--r--web-ui/app/js/mail_view/ui/mail_actions.js84
-rw-r--r--web-ui/app/js/mail_view/ui/mail_view.js255
-rw-r--r--web-ui/app/js/mail_view/ui/no_mails_available_pane.js50
-rw-r--r--web-ui/app/js/mail_view/ui/no_message_selected_pane.js41
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipient.js112
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipients.js193
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipients_input.js180
-rw-r--r--web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js59
-rw-r--r--web-ui/app/js/mail_view/ui/reply_box.js116
-rw-r--r--web-ui/app/js/mail_view/ui/reply_section.js129
-rw-r--r--web-ui/app/js/mail_view/ui/send_button.js130
-rw-r--r--web-ui/app/js/main.js84
-rw-r--r--web-ui/app/js/mixins/with_auto_refresh.js47
-rw-r--r--web-ui/app/js/mixins/with_compose_inline.js84
-rw-r--r--web-ui/app/js/mixins/with_enable_disable_on_event.js48
-rw-r--r--web-ui/app/js/mixins/with_feature_toggle.js40
-rw-r--r--web-ui/app/js/mixins/with_hide_and_show.js31
-rw-r--r--web-ui/app/js/mixins/with_mail_edit_base.js263
-rw-r--r--web-ui/app/js/mixins/with_mail_sandbox.js80
-rw-r--r--web-ui/app/js/mixins/with_mail_tagging.js69
-rw-r--r--web-ui/app/js/monkey_patching/all.js17
-rw-r--r--web-ui/app/js/monkey_patching/array.js27
-rw-r--r--web-ui/app/js/monkey_patching/post_message.js32
-rw-r--r--web-ui/app/js/page/default.js146
-rw-r--r--web-ui/app/js/page/events.js222
-rw-r--r--web-ui/app/js/page/logout.js43
-rw-r--r--web-ui/app/js/page/logout_shortcut.js33
-rw-r--r--web-ui/app/js/page/pane_contract_expand.js51
-rw-r--r--web-ui/app/js/page/pix_logo.js62
-rw-r--r--web-ui/app/js/page/router.js71
-rw-r--r--web-ui/app/js/page/router/url_params.js57
-rw-r--r--web-ui/app/js/page/unread_count_title.js53
-rw-r--r--web-ui/app/js/page/version.js41
-rw-r--r--web-ui/app/js/sandbox.js11
-rw-r--r--web-ui/app/js/search/results_highlighter.js97
-rw-r--r--web-ui/app/js/search/search_trigger.js81
-rw-r--r--web-ui/app/js/services/delete_service.js59
-rw-r--r--web-ui/app/js/services/mail_service.js335
-rw-r--r--web-ui/app/js/services/model/mail.js126
-rw-r--r--web-ui/app/js/services/recover_service.js38
-rw-r--r--web-ui/app/js/style_guide/main.js33
-rw-r--r--web-ui/app/js/tags/data/tags.js66
-rw-r--r--web-ui/app/js/tags/ui/tag.js154
-rw-r--r--web-ui/app/js/tags/ui/tag_base.js68
-rw-r--r--web-ui/app/js/tags/ui/tag_list.js105
-rw-r--r--web-ui/app/js/user_alerts/ui/user_alerts.js57
-rw-r--r--web-ui/app/js/user_settings/data/user_settings.js52
-rw-r--r--web-ui/app/js/user_settings/ui/user_settings_box.js77
-rw-r--r--web-ui/app/js/user_settings/ui/user_settings_icon.js57
-rw-r--r--web-ui/app/js/views/i18n.js62
-rw-r--r--web-ui/app/js/views/recipientListFormatter.js33
-rw-r--r--web-ui/app/js/views/templates.js85
96 files changed, 8444 insertions, 0 deletions
diff --git a/web-ui/app/js/dispatchers/left_pane_dispatcher.js b/web-ui/app/js/dispatchers/left_pane_dispatcher.js
new file mode 100644
index 00000000..0037a88f
--- /dev/null
+++ b/web-ui/app/js/dispatchers/left_pane_dispatcher.js
@@ -0,0 +1,62 @@
+/*
+ * 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/router/url_params',
+ 'page/events'
+ ],
+
+ function(defineComponent, urlParams, events) {
+ 'use strict';
+
+ return defineComponent(leftPaneDispatcher);
+
+ function leftPaneDispatcher() {
+ var initialized = false;
+
+ this.refreshTagList = function (ev, data) {
+ this.trigger(document, events.tags.want, { caller: this.$node, skipMailListRefresh: data.skipMailListRefresh });
+ };
+
+ this.loadTags = function (ev, data) {
+ this.trigger(document, events.ui.tagList.load, data);
+ };
+
+ this.selectTag = function (ev, data) {
+ var tag = (data && data.tag) || urlParams.getTag();
+ this.trigger(document, events.ui.tag.select, { tag: tag, skipMailListRefresh: data.skipMailListRefresh });
+ };
+
+ this.pushUrlState = function (ev, data) {
+ if (initialized) {
+ this.trigger(document, events.router.pushState, data);
+ }
+ initialized = true;
+ };
+
+ this.after('initialize', function () {
+ //this.on(this.$node, events.tags.received, this.loadTags);
+ this.on(document, events.dispatchers.tags.refreshTagList, this.refreshTagList);
+ this.on(document, events.ui.tags.loaded, this.selectTag);
+ this.on(document, events.ui.tag.selected, this.pushUrlState);
+ this.on(document, events.ui.tag.select, this.pushUrlState);
+ this.trigger(document, events.tags.want, { caller: this.$node });
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/dispatchers/middle_pane_dispatcher.js b/web-ui/app/js/dispatchers/middle_pane_dispatcher.js
new file mode 100644
index 00000000..12222aec
--- /dev/null
+++ b/web-ui/app/js/dispatchers/middle_pane_dispatcher.js
@@ -0,0 +1,74 @@
+/*
+ * 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', 'helpers/triggering', 'mail_view/ui/no_mails_available_pane'], function(defineComponent, events, triggering, NoMailsAvailablePane) {
+ 'use strict';
+
+ return defineComponent(function() {
+ this.defaultAttrs({
+ middlePane: '#middle-pane',
+ noMailsAvailablePane: 'no-mails-available-pane'
+ });
+
+ this.createChildDiv = function (component_id) {
+ var child_div = $('<div>', {id: component_id});
+ this.select('middlePane').append(child_div);
+ return child_div;
+ };
+
+ this.resetChildDiv = function(component_id) {
+ $('#' + component_id).remove();
+ };
+
+ this.refreshMailList = function (ev, data) {
+ this.trigger(document, events.ui.mails.fetchByTag, data);
+ };
+
+ this.cleanSelected = function(ev, data) {
+ this.trigger(document, events.ui.mails.cleanSelected);
+ };
+
+ this.resetScroll = function() {
+ this.select('middlePane').scrollTop(0);
+ };
+
+ this.updateMiddlePaneHeight = function() {
+ var vh = $(window).height();
+ var top = $('#main').outerHeight() + $('#top-pane').outerHeight();
+ this.select('middlePane').css({height: (vh - top) + 'px'});
+ };
+
+ this.onMailsChange = function (ev, data) {
+ this.resetChildDiv(this.attr.noMailsAvailablePane);
+ if (data.mails.length > 0) {
+ NoMailsAvailablePane.teardownAll();
+ } else {
+ var child_div = this.createChildDiv(this.attr.noMailsAvailablePane);
+ NoMailsAvailablePane.attachTo(child_div, {tag: data.tag, forSearch: data.forSearch});
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.middlePane.refreshMailList, this.refreshMailList);
+ this.on(document, events.dispatchers.middlePane.cleanSelected, this.cleanSelected);
+ this.on(document, events.dispatchers.middlePane.resetScroll, this.resetScroll);
+ this.on(document, events.mails.available, this.onMailsChange);
+
+ this.updateMiddlePaneHeight();
+ $(window).on('resize', this.updateMiddlePaneHeight.bind(this));
+ });
+ });
+});
diff --git a/web-ui/app/js/dispatchers/right_pane_dispatcher.js b/web-ui/app/js/dispatchers/right_pane_dispatcher.js
new file mode 100644
index 00000000..870bcd92
--- /dev/null
+++ b/web-ui/app/js/dispatchers/right_pane_dispatcher.js
@@ -0,0 +1,117 @@
+/*
+ * 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',
+ 'mail_view/ui/compose_box',
+ 'mail_view/ui/mail_view',
+ 'mail_view/ui/reply_section',
+ 'mail_view/ui/draft_box',
+ 'mail_view/ui/no_message_selected_pane',
+ 'mail_view/ui/feedback_box',
+ 'page/events'
+ ],
+
+ function(defineComponent, ComposeBox, MailView, ReplySection, DraftBox, NoMessageSelectedPane, FeedbackBox, events) {
+ 'use strict';
+
+ return defineComponent(rightPaneDispatcher);
+
+ function rightPaneDispatcher() {
+ this.defaultAttrs({
+ rightPane: '#right-pane',
+ composeBox: 'compose-box',
+ feedbackBox: 'feedback-box',
+ mailView: 'mail-view',
+ noMessageSelectedPane: 'no-message-selected-pane',
+ replySection: 'reply-section',
+ draftBox: 'draft-box',
+ currentTag: ''
+ });
+
+ this.createAndAttach = function(newContainer) {
+ var stage = $('<div>', { id: newContainer });
+ this.select('rightPane').append(stage);
+ return stage;
+ };
+
+ this.reset = function (newContainer) {
+ this.trigger(document, events.dispatchers.rightPane.clear);
+ this.select('rightPane').empty();
+ var stage = this.createAndAttach(newContainer);
+ return stage;
+ };
+
+ this.openComposeBox = function() {
+ var stage = this.reset(this.attr.composeBox);
+ ComposeBox.attachTo(stage, {currentTag: this.attr.currentTag});
+ };
+
+ this.openFeedbackBox = function() {
+ var stage = this.reset(this.attr.feedbackBox);
+ FeedbackBox.attachTo(stage);
+ };
+
+ this.openMail = function(ev, data) {
+ var stage = this.reset(this.attr.mailView);
+ MailView.attachTo(stage, data);
+
+ var replySectionContainer = this.createAndAttach(this.attr.replySection);
+ ReplySection.attachTo(replySectionContainer, { ident: data.ident });
+ };
+
+ this.initializeNoMessageSelectedPane = function () {
+ var stage = this.reset(this.attr.noMessageSelectedPane);
+ NoMessageSelectedPane.attachTo(stage);
+ this.trigger(document, events.dispatchers.middlePane.cleanSelected);
+ };
+
+ this.openNoMessageSelectedPane = function(ev, data) {
+ this.initializeNoMessageSelectedPane();
+
+ this.trigger(document, events.router.pushState, { tag: this.attr.currentTag, isDisplayNoMessageSelected: true });
+ };
+
+ this.openDraft = function (ev, data) {
+ var stage = this.reset(this.attr.draftBox);
+ DraftBox.attachTo(stage, { mailIdent: data.ident, currentTag: this.attr.currentTag });
+ };
+
+ this.selectTag = function(ev, data) {
+ this.trigger(document, events.ui.tags.loaded, {tag: data.tag});
+ };
+
+ this.saveTag = function(ev, data) {
+ this.attr.currentTag = data.tag;
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.rightPane.openComposeBox, this.openComposeBox);
+ this.on(document, events.dispatchers.rightPane.openDraft, this.openDraft);
+ this.on(document, events.ui.mail.open, this.openMail);
+ this.on(document, events.dispatchers.rightPane.openFeedbackBox, this.openFeedbackBox);
+ this.on(document, events.dispatchers.rightPane.openNoMessageSelected, this.openNoMessageSelectedPane);
+ this.on(document, events.dispatchers.rightPane.selectTag, this.selectTag);
+ this.on(document, events.ui.tag.selected, this.saveTag);
+ this.on(document, events.ui.tag.select, this.saveTag);
+ this.on(document, events.dispatchers.rightPane.openNoMessageSelectedWithoutPushState, this.initializeNoMessageSelectedPane);
+ this.initializeNoMessageSelectedPane();
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/features/features.js b/web-ui/app/js/features/features.js
new file mode 100644
index 00000000..f71d56ea
--- /dev/null
+++ b/web-ui/app/js/features/features.js
@@ -0,0 +1,61 @@
+/*
+ * 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/monitored_ajax'], function(monitoredAjax) {
+ 'use strict';
+ var cachedDisabledFeatures;
+ var cachedMultiUserFeatures;
+
+ function getDisabledFeatures() {
+ cachedDisabledFeatures = cachedDisabledFeatures || fetchFeatures().disabled_features;
+ return cachedDisabledFeatures;
+ }
+
+ function getMultiUserFeatures() {
+ cachedMultiUserFeatures = cachedMultiUserFeatures || fetchFeatures().multi_user;
+ return cachedMultiUserFeatures;
+ }
+
+ function fetchFeatures() {
+ var features;
+ monitoredAjax(this, '/features', {
+ async: false,
+ success: function (results) {
+ features = results;
+ },
+ error: function () {
+ console.error('Could not load feature toggles');
+ }
+ });
+ return features;
+ }
+
+ return {
+ isEnabled: function (featureName) {
+ return ! _.contains(getDisabledFeatures(), featureName);
+ },
+ isAutoRefreshEnabled: function () {
+ return this.isEnabled('autoRefresh');
+ },
+ isLogoutEnabled: function () {
+ return _.has(getMultiUserFeatures(), 'logout');
+ },
+ getLogoutUrl: function () {
+ return getMultiUserFeatures().logout;
+ }
+ };
+});
diff --git a/web-ui/app/js/feedback/feedback_cache.js b/web-ui/app/js/feedback/feedback_cache.js
new file mode 100644
index 00000000..a5d92266
--- /dev/null
+++ b/web-ui/app/js/feedback/feedback_cache.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2016 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([], function() {
+ 'use strict';
+
+ return (function() {
+ var feedbackCache = '';
+ return {
+ resetCache: function () {
+ feedbackCache = '';
+ },
+ setCache: function(feedback) {
+ feedbackCache = feedback;
+ },
+ getCache: function() {
+ return feedbackCache;
+ }
+ };
+ })();
+});
diff --git a/web-ui/app/js/feedback/feedback_trigger.js b/web-ui/app/js/feedback/feedback_trigger.js
new file mode 100644
index 00000000..598f9060
--- /dev/null
+++ b/web-ui/app/js/feedback/feedback_trigger.js
@@ -0,0 +1,39 @@
+/*
+ * 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'],
+ function (defineComponent, templates, events, features) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.render = function () {
+ this.$node.html(templates.feedback.feedback());
+ };
+
+ this.onClick = function() {
+ this.trigger(document, events.dispatchers.rightPane.openFeedbackBox);
+ };
+
+ this.after('initialize', function () {
+ if (features.isEnabled('feedback')) {
+ this.render();
+ this.on('click', this.onClick);
+ }
+ });
+
+ });
+});
diff --git a/web-ui/app/js/foundation/initialize_foundation.js b/web-ui/app/js/foundation/initialize_foundation.js
new file mode 100644
index 00000000..42405dfe
--- /dev/null
+++ b/web-ui/app/js/foundation/initialize_foundation.js
@@ -0,0 +1,5 @@
+
+(function() {
+ 'use strict';
+ $(document).foundation();
+})();
diff --git a/web-ui/app/js/foundation/off_canvas.js b/web-ui/app/js/foundation/off_canvas.js
new file mode 100644
index 00000000..66334470
--- /dev/null
+++ b/web-ui/app/js/foundation/off_canvas.js
@@ -0,0 +1,46 @@
+/*
+ * 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'], function (defineComponent, events) {
+ 'use strict';
+ return defineComponent(function() {
+
+ this.closeSlider = function (ev){
+ $('.off-canvas-wrap.content').removeClass('move-right');
+ this.toggleTagsVisibility();
+ };
+
+ this.toggleSlideContent = function (ev) {
+ ev.preventDefault();
+ $('.left-off-canvas-toggle').click();
+ this.toggleTagsVisibility();
+ };
+
+ this.toggleTagsVisibility = function () {
+ if ($('.off-canvas-wrap.content').hasClass('move-right')) {
+ $('#custom-tag-list').addClass('expanded');
+ } else {
+ $('#custom-tag-list').removeClass('expanded');
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on($('#middle-pane-container'), 'click', this.closeSlider);
+ this.on($('#right-pane'), 'click', this.closeSlider);
+ this.on($('.side-nav-toggle'), 'click', this.toggleSlideContent);
+ });
+ });
+});
diff --git a/web-ui/app/js/helpers/browser.js b/web-ui/app/js/helpers/browser.js
new file mode 100644
index 00000000..dacf2263
--- /dev/null
+++ b/web-ui/app/js/helpers/browser.js
@@ -0,0 +1,36 @@
+/*
+ * 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([], function () {
+
+ 'use strict';
+
+ function redirect(url) {
+ window.location.replace(url);
+ }
+
+ function getCookie(name) {
+ var value = '; ' + document.cookie;
+ var parts = value.split('; ' + name + '=');
+ if (parts.length === 2) { return parts.pop().split(';').shift(); }
+ }
+
+ return {
+ redirect: redirect,
+ getCookie: getCookie
+ };
+});
diff --git a/web-ui/app/js/helpers/contenttype.js b/web-ui/app/js/helpers/contenttype.js
new file mode 100644
index 00000000..a1e5361a
--- /dev/null
+++ b/web-ui/app/js/helpers/contenttype.js
@@ -0,0 +1,184 @@
+/*
+ * 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/>.
+ */
+
+/* jshint curly: false */
+define([], function () {
+ 'use strict';
+ var exports = {};
+
+ // Licence: PUBLIC DOMAIN <http://unlicense.org/>
+ // Author: Austin Wright <http://github.com/Acubed>
+
+ function MediaType(s, p){
+ this.type = '';
+ this.params = {};
+ var c, i, n;
+ if(typeof s==='string'){
+ c = splitQuotedString(s);
+ this.type = c.shift();
+ for(i=0; i<c.length; i++){
+ this.parseParameter(c[i]);
+ }
+ }else if(s instanceof MediaType){
+ this.type = s.type;
+ this.q = s.q;
+ for(n in s.params) this.params[n]=s.params[n];
+ }
+ if(typeof p==='string'){
+ c = splitQuotedString(p);
+ for(i=0; i<c.length; i++){
+ this.parseParameter(c[i]);
+ }
+ }else if(typeof p==='object'){
+ for(n in p) this.params[n]=p[n];
+ }
+ }
+ MediaType.prototype.parseParameter = function parseParameter(s){
+ var param = s.split('=',1);
+ var name = param[0].trim();
+ var value = s.substr(param[0].length+1).trim();
+ if(!value || !name) return;
+ if(name==='q' && this.q===undefined){
+ this.q=parseFloat(value);
+ }else{
+ if(value[0]==='"' && value[value.length-1]==='"'){
+ value = value.substr(1, value.length-2);
+ value = value.replace(/\\(.)/g, function(a,b){return b;});
+ }
+ this.params[name]=value;
+ }
+ };
+ MediaType.prototype.toString = function toString(){
+ var str = this.type + ';q='+this.q;
+ for(var n in this.params){
+ str += ';'+n+'=';
+ if(this.params[n].match(/["=;<>\[\]\(\) ,\-]/)){
+ str += '"' + this.params[n].replace(/["\\]/g, function(a){return '\\'+a;}) + '"';
+ }else{
+ str += this.params[n];
+ }
+ }
+ return str;
+ };
+ exports.MediaType = MediaType;
+
+ // Split a string by character, but ignore quoted parts and backslash-escaped characters
+ function splitQuotedString(str, delim, quote){
+ delim = delim || ';';
+ quote = quote || '"';
+ var res = [];
+ var start = 0;
+ var offset = 0;
+ function findNextChar(v, c, i, a){
+ var p = str.indexOf(c, offset+1);
+ return (p<0)?v:Math.min(p,v);
+ }
+ while(offset>=0){
+ offset = [delim,quote].reduce(findNextChar, 1/0);
+ if(offset===1/0) break;
+ switch(str[offset]){
+ case quote:
+ // Skip to end of quoted string
+ while(1){
+ offset=str.indexOf(quote, offset+1);
+ if(offset<0) break;
+ if(str[offset-1]==='\\') continue;
+ break;
+ }
+ continue;
+ case delim:
+ res.push(str.substr(start, offset-start).trim());
+ start = ++offset;
+ break;
+ }
+ }
+ res.push(str.substr(start).trim());
+ return res;
+ }
+ exports.splitQuotedString = splitQuotedString;
+
+ // Split a list of content types found in an Accept header
+ // Maybe use it like: splitContentTypes(request.headers.accept).map(parseMedia)
+ function splitContentTypes(str){
+ return splitQuotedString(str, ',');
+ }
+ exports.splitContentTypes = splitContentTypes;
+
+ function parseMedia(str){
+ var o = new MediaType(str);
+ if(o.q===undefined) o.q=1;
+ return o;
+ }
+ exports.parseMedia = parseMedia;
+
+ // Pick an ideal representation to send given a list of representations to choose from and the client-preferred list
+ function select(reps, accept){
+ var cr = {q:0};
+ var ca = {q:0};
+ var cq = 0;
+ for(var i=0; i<reps.length; i++){
+ var r = reps[i];
+ var rq = r.q || 1;
+ for(var j=0; j<accept.length; j++){
+ var a=accept[j];
+ var aq = a.q || 1;
+ var cmp = mediaCmp(a, r);
+ if(cmp!==null && cmp>=0){
+ if(aq*rq>cq){
+ ca = a;
+ cr = r;
+ cq = ca.q*cr.q;
+ if(cq===1 && cr.type) return cr;
+ }
+ }
+ }
+ }
+ return cr.type&&cr;
+ }
+ exports.select = select;
+
+ // Determine if one media type is a subset of another
+ // If a is a superset of b (b is smaller than a), return 1
+ // If b is a superset of a, return -1
+ // If they are the exact same, return 0
+ // If they are disjoint, return null
+ function mediaCmp(a, b){
+ if(a.type==='*/*' && b.type!=='*/*') return 1;
+ else if(a.type!=='*/*' && b.type==='*/*') return -1;
+ var ac = (a.type||'').split('/');
+ var bc = (b.type||'').split('/');
+ if(ac[0]==='*' && bc[0]!=='*') return 1;
+ if(ac[0]!=='*' && bc[0]==='*') return -1;
+ if(a.type!==b.type) return null;
+ var ap = a.params || {};
+ var bp = b.params || {};
+ var ak = Object.keys(ap);
+ var bk = Object.keys(bp);
+ if(ak.length < bk.length) return 1;
+ if(ak.length > bk.length) return -1;
+ var k = ak.concat(bk).sort();
+ var dir = 0;
+ for(var n in ap){
+ if(ap[n] && !bp[n]){ if(dir<0) return null; else dir=1; }
+ if(!ap[n] && bp[n]){ if(dir>0) return null; else dir=-1; }
+ }
+ return dir;
+ }
+ exports.mediaCmp = mediaCmp;
+
+ return exports;
+});
diff --git a/web-ui/app/js/helpers/iterator.js b/web-ui/app/js/helpers/iterator.js
new file mode 100644
index 00000000..236c7a40
--- /dev/null
+++ b/web-ui/app/js/helpers/iterator.js
@@ -0,0 +1,60 @@
+/*
+ * 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(function () {
+ 'use strict';
+
+ return Iterator;
+
+ function Iterator(elems, startingIndex) {
+
+ this.index = startingIndex || 0;
+ this.elems = elems;
+
+ this.hasPrevious = function () {
+ return this.index !== 0;
+ };
+
+ this.hasNext = function () {
+ return this.index < this.elems.length - 1;
+ };
+
+ this.previous = function () {
+ return this.elems[--this.index];
+ };
+
+ this.next = function () {
+ return this.elems[++this.index];
+ };
+
+ this.current = function () {
+ return this.elems[this.index];
+ };
+
+ this.hasElements = function () {
+ return this.elems.length > 0;
+ };
+
+ this.removeCurrent = function () {
+ var removed = this.current(),
+ toRemove = this.index;
+
+ if(!this.hasNext()) { this.index--; }
+ this.elems.remove(toRemove);
+ return removed;
+ };
+ }
+});
diff --git a/web-ui/app/js/helpers/monitored_ajax.js b/web-ui/app/js/helpers/monitored_ajax.js
new file mode 100644
index 00000000..bbf85c45
--- /dev/null
+++ b/web-ui/app/js/helpers/monitored_ajax.js
@@ -0,0 +1,67 @@
+/*
+ * 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(['page/events', 'views/i18n', 'helpers/browser'], function (events, i18n, browser) {
+
+ 'use strict';
+
+ var messages = {
+ timeout: 'error.timeout',
+ error: 'error.general',
+ parseerror: 'error.parse'
+ };
+
+ function monitoredAjax(on, url, config) {
+ config = config || {};
+ config.timeout = 60 * 1000;
+
+ var originalBeforeSend = config.beforeSend;
+ config.beforeSend = function () {
+ if (originalBeforeSend) {
+ originalBeforeSend();
+ }
+ };
+
+ config.headers = {'X-XSRF-TOKEN': browser.getCookie('XSRF-TOKEN')};
+
+ var originalComplete = config.complete;
+ config.complete = function () {
+ if (originalComplete) {
+ originalComplete();
+ }
+ };
+
+ return $.ajax(url, config).fail(function (xmlhttprequest, textstatus, message) {
+ if (!config.skipErrorMessage) {
+ var msg = (xmlhttprequest.responseJSON && xmlhttprequest.responseJSON.message) ||
+ messages[textstatus] || messages.error;
+ on.trigger(document, events.ui.userAlerts.displayMessage, {message: i18n.t(msg), class: 'error'});
+ }
+
+ if (xmlhttprequest.status === 302) {
+ var redirectUrl = xmlhttprequest.getResponseHeader('Location');
+ browser.redirect(redirectUrl);
+ } else if (xmlhttprequest.status === 401) {
+ browser.redirect('/');
+ }
+
+ }.bind(this));
+ }
+
+ return monitoredAjax;
+
+});
diff --git a/web-ui/app/js/helpers/sanitizer.js b/web-ui/app/js/helpers/sanitizer.js
new file mode 100644
index 00000000..443e8602
--- /dev/null
+++ b/web-ui/app/js/helpers/sanitizer.js
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2016 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(['DOMPurify', 'he'], function (DOMPurify, he) {
+ 'use strict';
+
+ /**
+ * Sanitizes a mail body to safe-to-display HTML
+ */
+ var sanitizer = {};
+
+ sanitizer.whitelist = [{
+ // highlight tag open
+ pre: '&#x3C;&#x65;&#x6D;&#x20;&#x63;&#x6C;&#x61;&#x73;&#x73;&#x3D;&#x22;&#x73;&#x65;&#x61;&#x72;&#x63;&#x68;&#x2D;&#x68;&#x69;&#x67;&#x68;&#x6C;&#x69;&#x67;&#x68;&#x74;&#x22;&#x3E;',
+ post: '<em class="search-highlight">'
+ }, {
+ // highlight tag close
+ pre: '&#x3C;&#x2F;&#x65;&#x6D;&#x3E;',
+ post: '</em>'
+ }];
+
+ /**
+ * Adds html line breaks to a plaintext with line breaks (incl carriage return)
+ *
+ * @param {string} textPlainBody Plaintext input
+ * @returns {string} Plaintext with HTML line breals (<br/>)
+ */
+ sanitizer.addLineBreaks = function (textPlainBody) {
+ return textPlainBody.replace(/(\r)?\n/g, '<br/>').replace(/(&#xD;)?&#xA;/g, '<br/>');
+ };
+
+ /**
+ * Runs a given dirty body through DOMPurify, thereby removing
+ * potentially hazardous XSS attacks. Please be advised that this
+ * will not act as a privacy leak prevention. Contained contents
+ * will still point to remote sources.
+ *
+ * For future reference: Running DOMPurify with these parameters
+ * can help mitigate some of the most widely used privacy leaks.
+ * FORBID_TAGS: ['style', 'svg', 'audio', 'video', 'math'],
+ * FORBID_ATTR: ['src']
+ *
+ * @param {string} dirtyBody The unsanitized string
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.purifyHtml = function (dirtyBody) {
+ return DOMPurify.sanitize(dirtyBody, {
+ SAFE_FOR_JQUERY: true,
+ SAFE_FOR_TEMPLATES: true
+ });
+ };
+
+ /**
+ * Runs a given dirty body through he, thereby encoding everything
+ * as HTML entities.
+ *
+ * @param {string} dirtyBody The unsanitized string
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.purifyText = function (dirtyBody) {
+ var escapedBody = he.encode(dirtyBody, {
+ encodeEverything: true
+ });
+
+ this.whitelist.forEach(function(entry) {
+ while (escapedBody.indexOf(entry.pre) > -1) {
+ escapedBody = escapedBody.replace(entry.pre, entry.post);
+ }
+ });
+
+ return escapedBody;
+ };
+
+ /**
+ * Calls #purify and #addLineBreaks to turn untrusted mail body content
+ * into safe-to-display HTML.
+ *
+ * NB: HTML content is preferred to plaintext content.
+ *
+ * @param {object} mail Pixelated Mail Object
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.sanitize = function (mail) {
+ var body;
+
+ if (mail.htmlBody) {
+ body = this.purifyHtml(mail.htmlBody);
+ } else {
+ body = this.purifyText(mail.textPlainBody);
+ body = this.addLineBreaks(body);
+ }
+
+ return body;
+ };
+
+ /**
+ * Add hooks to DOMPurify for opening links in new windows
+ */
+ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
+ // set all elements owning target to target=_blank
+ if ('target' in node) {
+ node.setAttribute('target', '_blank');
+ }
+
+ // set non-HTML/MathML links to xlink:show=new
+ if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
+ node.setAttribute('xlink:show', 'new');
+ }
+ });
+
+ return sanitizer;
+});
diff --git a/web-ui/app/js/helpers/triggering.js b/web-ui/app/js/helpers/triggering.js
new file mode 100644
index 00000000..d26d9fc6
--- /dev/null
+++ b/web-ui/app/js/helpers/triggering.js
@@ -0,0 +1,29 @@
+/*
+ * 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([], function() {
+ 'use strict';
+
+ return function(that, event, data, on) {
+ return function() {
+ if(on) {
+ that.trigger(on, event, data || {});
+ } else {
+ that.trigger(event, data || {});
+ }
+ };
+ };
+});
diff --git a/web-ui/app/js/helpers/view_helper.js b/web-ui/app/js/helpers/view_helper.js
new file mode 100644
index 00000000..ed9e0559
--- /dev/null
+++ b/web-ui/app/js/helpers/view_helper.js
@@ -0,0 +1,163 @@
+/*
+ * 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/contenttype',
+ 'views/i18n',
+ 'quoted-printable/quoted-printable',
+ 'utf8/utf8',
+ 'helpers/sanitizer'
+ ],
+ function(contentType, i18n, quotedPrintable, utf8, sanitizer) {
+ 'use strict';
+
+ function formatStatusClasses(ss) {
+ return _.map(ss, function(s) {
+ return 'status-' + s;
+ }).join(' ');
+ }
+
+ function formatMailBody(mail) {
+ return sanitizer.sanitize(mail);
+ }
+
+ function moveCaretToEnd(el) {
+ if (typeof el.selectionStart === 'number') {
+ el.selectionStart = el.selectionEnd = el.value.length;
+ } else if (typeof el.createTextRange !== 'undefined') {
+ el.focus();
+ var range = el.createTextRange();
+ range.collapse(false);
+ range.select();
+ }
+ }
+
+ function fixedSizeNumber(num, size) {
+ var res = num.toString();
+ while(res.length < size) {
+ res = '0' + res;
+ }
+ return res;
+ }
+
+ function createTodayDate() {
+ var today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ return today;
+ }
+
+ function moveCaretToEndOfText() {
+ var self = this;
+
+ moveCaretToEnd(self);
+ window.setTimeout(function() {
+ moveCaretToEnd(self);
+ }, 1);
+ }
+
+ function prependFrom(mail) {
+ return i18n.t(
+ 'reply-author-line', {'date': new Date(mail.header.date).toString(), 'from': mail.header.from}
+ );
+ }
+
+ function quoteMail(mail) {
+ return '\n\n' + prependFrom(mail) + mail.textPlainBody.replace(/^/mg, '> ');
+ }
+
+ function formatDate(dateString) {
+ var date = new Date(dateString);
+ var today = createTodayDate();
+ if (date.getTime() > today.getTime()) {
+ return fixedSizeNumber(date.getHours(), 2) + ':' + fixedSizeNumber(date.getMinutes(), 2);
+ } else {
+ return '' + date.getFullYear() + '-' + fixedSizeNumber(date.getMonth() + 1, 2) + '-' + fixedSizeNumber(date.getDate(), 2);
+ }
+ }
+
+ function formatSize(bytes) {
+ var e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'b';
+ }
+
+
+ function formatFingerPrint(fingerprint) {
+ fingerprint = fingerprint || '';
+ return fingerprint.replace(/(.{4})/g, '$1 ').trim();
+ }
+
+ function getSinceDate(sinceDate){
+ var commitDate = new Date(sinceDate);
+ var number = Date.now();
+ var millisecondsSince = number - commitDate;
+
+ var SECONDS = 1000,
+ MIN = 60 * SECONDS,
+ HOUR = MIN * 60,
+ DAY = HOUR * 24,
+ WEEK = DAY * 7,
+ MONTH = WEEK * 4,
+ YEAR = DAY * 365;
+
+ var years = Math.floor(millisecondsSince / YEAR);
+ if (years >= 1){
+ return years + ' year(s)';
+ }
+
+ var months = Math.floor(millisecondsSince / MONTH);
+ if (months >= 1) {
+ return months + ' month(s)';
+ }
+
+ var weeks = Math.floor(millisecondsSince / WEEK);
+ if (weeks >= 1) {
+ return weeks + ' week(s)';
+ }
+
+ var days = Math.floor(millisecondsSince / DAY);
+ if (days >= 1) {
+ return days + ' day(s)';
+ }
+
+ var hours = Math.floor(millisecondsSince / HOUR);
+ if (hours >= 1) {
+ return hours + ' hour(s)';
+ }
+
+ var minutes = Math.floor(millisecondsSince / MIN);
+ return minutes + ' minute(s)';
+ }
+
+ Handlebars.registerHelper('formatDate', formatDate);
+ Handlebars.registerHelper('formatSize', formatSize);
+ Handlebars.registerHelper('formatStatusClasses', formatStatusClasses);
+ Handlebars.registerHelper('formatFingerPrint', formatFingerPrint);
+ Handlebars.registerHelper('sinceDate', getSinceDate);
+
+ return {
+ formatStatusClasses: formatStatusClasses,
+ formatSize: formatSize,
+ formatMailBody: formatMailBody,
+ formatFingerPrint: formatFingerPrint,
+ moveCaretToEndOfText: moveCaretToEndOfText,
+ quoteMail: quoteMail,
+ sinceDate: getSinceDate,
+ i18n: i18n
+ };
+});
diff --git a/web-ui/app/js/lib/highlightRegex.js b/web-ui/app/js/lib/highlightRegex.js
new file mode 100644
index 00000000..17caaa23
--- /dev/null
+++ b/web-ui/app/js/lib/highlightRegex.js
@@ -0,0 +1,127 @@
+/*
+ * jQuery Highlight Regex Plugin v0.1.2
+ *
+ * Based on highlight v3 by Johann Burkard
+ * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
+ *
+ * (c) 2009-13 Jacob Rothstein
+ * MIT license
+ */
+
+;(function( $ ) {
+
+
+
+ var normalize = function( node ) {
+ if ( ! ( node && node.childNodes )) return
+
+ var children = $.makeArray( node.childNodes )
+ , prevTextNode = null
+
+ $.each( children, function( i, child ) {
+ if ( child.nodeType === 3 ) {
+ if ( child.nodeValue === "" ) {
+
+ node.removeChild( child )
+
+ } else if ( prevTextNode !== null ) {
+
+ prevTextNode.nodeValue += child.nodeValue;
+ node.removeChild( child )
+
+ } else {
+
+ prevTextNode = child
+
+ }
+ } else {
+ prevTextNode = null
+
+ if ( child.childNodes ) {
+ normalize( child )
+ }
+ }
+ })
+ }
+
+
+
+
+ $.fn.highlightRegex = function( regex, options ) {
+
+ if ( typeof regex === 'object' && !(regex.constructor.name == 'RegExp' || regex instanceof RegExp ) ) {
+ options = regex
+ regex = undefined
+ }
+
+ if ( typeof options === 'undefined' ) options = {}
+
+ options.className = options.className || 'highlight'
+ options.tagType = options.tagType || 'span'
+ options.attrs = options.attrs || {}
+
+ if ( typeof regex === 'undefined' || regex.source === '' ) {
+
+ $( this ).find( options.tagType + '.' + options.className ).each( function() {
+
+ $( this ).replaceWith( $( this ).text() )
+
+ normalize( $( this ).parent().get( 0 ))
+
+ })
+
+ } else {
+
+ $( this ).each( function() {
+
+ var elt = $( this ).get( 0 )
+
+ normalize( elt )
+
+ $.each( $.makeArray( elt.childNodes ), function( i, searchnode ) {
+
+ var spannode, middlebit, middleclone, pos, match, parent
+
+ normalize( searchnode )
+
+ if ( searchnode.nodeType == 3 ) {
+
+ // don't re-highlight the same node over and over
+ if ( $(searchnode).parent(options.tagType + '.' + options.className).length ) {
+ return;
+ }
+
+ while ( searchnode.data &&
+ ( pos = searchnode.data.search( regex )) >= 0 ) {
+
+ match = searchnode.data.slice( pos ).match( regex )[ 0 ]
+
+ if ( match.length > 0 ) {
+
+ spannode = document.createElement( options.tagType )
+ spannode.className = options.className
+ $(spannode).attr(options.attrs)
+
+ parent = searchnode.parentNode
+ middlebit = searchnode.splitText( pos )
+ searchnode = middlebit.splitText( match.length )
+ middleclone = middlebit.cloneNode( true )
+
+ spannode.appendChild( middleclone )
+ parent.replaceChild( spannode, middlebit )
+
+ } else break
+ }
+
+ } else {
+
+ $( searchnode ).highlightRegex( regex, options )
+
+ }
+ })
+ })
+ }
+
+ return $( this )
+ }
+})( jQuery );
diff --git a/web-ui/app/js/lib/html4-defs.js b/web-ui/app/js/lib/html4-defs.js
new file mode 100644
index 00000000..1ec575da
--- /dev/null
+++ b/web-ui/app/js/lib/html4-defs.js
@@ -0,0 +1,640 @@
+// Copyright Google Inc.
+// Licensed under the Apache Licence Version 2.0
+// Autogenerated at Mon Jul 14 18:51:33 BRT 2014
+// @overrides window
+// @provides html4
+define([], function() {
+var html4 = {};
+html4.atype = {
+ 'NONE': 0,
+ 'URI': 1,
+ 'URI_FRAGMENT': 11,
+ 'SCRIPT': 2,
+ 'STYLE': 3,
+ 'HTML': 12,
+ 'ID': 4,
+ 'IDREF': 5,
+ 'IDREFS': 6,
+ 'GLOBAL_NAME': 7,
+ 'LOCAL_NAME': 8,
+ 'CLASSES': 9,
+ 'FRAME_TARGET': 10,
+ 'MEDIA_QUERY': 13
+};
+html4[ 'atype' ] = html4.atype;
+html4.ATTRIBS = {
+ '*::class': 9,
+ '*::dir': 0,
+ '*::draggable': 0,
+ '*::hidden': 0,
+ '*::id': 4,
+ '*::inert': 0,
+ '*::itemprop': 0,
+ '*::itemref': 6,
+ '*::itemscope': 0,
+ '*::lang': 0,
+ '*::onblur': 2,
+ '*::onchange': 2,
+ '*::onclick': 2,
+ '*::ondblclick': 2,
+ '*::onerror': 2,
+ '*::onfocus': 2,
+ '*::onkeydown': 2,
+ '*::onkeypress': 2,
+ '*::onkeyup': 2,
+ '*::onload': 2,
+ '*::onmousedown': 2,
+ '*::onmousemove': 2,
+ '*::onmouseout': 2,
+ '*::onmouseover': 2,
+ '*::onmouseup': 2,
+ '*::onreset': 2,
+ '*::onscroll': 2,
+ '*::onselect': 2,
+ '*::onsubmit': 2,
+ '*::ontouchcancel': 2,
+ '*::ontouchend': 2,
+ '*::ontouchenter': 2,
+ '*::ontouchleave': 2,
+ '*::ontouchmove': 2,
+ '*::ontouchstart': 2,
+ '*::onunload': 2,
+ '*::spellcheck': 0,
+ '*::style': 3,
+ '*::tabindex': 0,
+ '*::title': 0,
+ '*::translate': 0,
+ 'a::accesskey': 0,
+ 'a::coords': 0,
+ 'a::href': 1,
+ 'a::hreflang': 0,
+ 'a::name': 7,
+ 'a::onblur': 2,
+ 'a::onfocus': 2,
+ 'a::shape': 0,
+ 'a::target': 10,
+ 'a::type': 0,
+ 'area::accesskey': 0,
+ 'area::alt': 0,
+ 'area::coords': 0,
+ 'area::href': 1,
+ 'area::nohref': 0,
+ 'area::onblur': 2,
+ 'area::onfocus': 2,
+ 'area::shape': 0,
+ 'area::target': 10,
+ 'audio::controls': 0,
+ 'audio::loop': 0,
+ 'audio::mediagroup': 5,
+ 'audio::muted': 0,
+ 'audio::preload': 0,
+ 'audio::src': 1,
+ 'bdo::dir': 0,
+ 'blockquote::cite': 1,
+ 'br::clear': 0,
+ 'button::accesskey': 0,
+ 'button::disabled': 0,
+ 'button::name': 8,
+ 'button::onblur': 2,
+ 'button::onfocus': 2,
+ 'button::type': 0,
+ 'button::value': 0,
+ 'canvas::height': 0,
+ 'canvas::width': 0,
+ 'caption::align': 0,
+ 'col::align': 0,
+ 'col::char': 0,
+ 'col::charoff': 0,
+ 'col::span': 0,
+ 'col::valign': 0,
+ 'col::width': 0,
+ 'colgroup::align': 0,
+ 'colgroup::char': 0,
+ 'colgroup::charoff': 0,
+ 'colgroup::span': 0,
+ 'colgroup::valign': 0,
+ 'colgroup::width': 0,
+ 'command::checked': 0,
+ 'command::command': 5,
+ 'command::disabled': 0,
+ 'command::icon': 1,
+ 'command::label': 0,
+ 'command::radiogroup': 0,
+ 'command::type': 0,
+ 'data::value': 0,
+ 'del::cite': 1,
+ 'del::datetime': 0,
+ 'details::open': 0,
+ 'dir::compact': 0,
+ 'div::align': 0,
+ 'dl::compact': 0,
+ 'fieldset::disabled': 0,
+ 'font::color': 0,
+ 'font::face': 0,
+ 'font::size': 0,
+ 'form::accept': 0,
+ 'form::action': 1,
+ 'form::autocomplete': 0,
+ 'form::enctype': 0,
+ 'form::method': 0,
+ 'form::name': 7,
+ 'form::novalidate': 0,
+ 'form::onreset': 2,
+ 'form::onsubmit': 2,
+ 'form::target': 10,
+ 'h1::align': 0,
+ 'h2::align': 0,
+ 'h3::align': 0,
+ 'h4::align': 0,
+ 'h5::align': 0,
+ 'h6::align': 0,
+ 'hr::align': 0,
+ 'hr::noshade': 0,
+ 'hr::size': 0,
+ 'hr::width': 0,
+ 'iframe::align': 0,
+ 'iframe::frameborder': 0,
+ 'iframe::height': 0,
+ 'iframe::marginheight': 0,
+ 'iframe::marginwidth': 0,
+ 'iframe::width': 0,
+ 'img::align': 0,
+ 'img::alt': 0,
+ 'img::border': 0,
+ 'img::height': 0,
+ 'img::hspace': 0,
+ 'img::ismap': 0,
+ 'img::name': 7,
+ 'img::src': 1,
+ 'img::usemap': 11,
+ 'img::vspace': 0,
+ 'img::width': 0,
+ 'input::accept': 0,
+ 'input::accesskey': 0,
+ 'input::align': 0,
+ 'input::alt': 0,
+ 'input::autocomplete': 0,
+ 'input::checked': 0,
+ 'input::disabled': 0,
+ 'input::inputmode': 0,
+ 'input::ismap': 0,
+ 'input::list': 5,
+ 'input::max': 0,
+ 'input::maxlength': 0,
+ 'input::min': 0,
+ 'input::multiple': 0,
+ 'input::name': 8,
+ 'input::onblur': 2,
+ 'input::onchange': 2,
+ 'input::onfocus': 2,
+ 'input::onselect': 2,
+ 'input::pattern': 0,
+ 'input::placeholder': 0,
+ 'input::readonly': 0,
+ 'input::required': 0,
+ 'input::size': 0,
+ 'input::src': 1,
+ 'input::step': 0,
+ 'input::type': 0,
+ 'input::usemap': 11,
+ 'input::value': 0,
+ 'ins::cite': 1,
+ 'ins::datetime': 0,
+ 'label::accesskey': 0,
+ 'label::for': 5,
+ 'label::onblur': 2,
+ 'label::onfocus': 2,
+ 'legend::accesskey': 0,
+ 'legend::align': 0,
+ 'li::type': 0,
+ 'li::value': 0,
+ 'map::name': 7,
+ 'menu::compact': 0,
+ 'menu::label': 0,
+ 'menu::type': 0,
+ 'meter::high': 0,
+ 'meter::low': 0,
+ 'meter::max': 0,
+ 'meter::min': 0,
+ 'meter::value': 0,
+ 'ol::compact': 0,
+ 'ol::reversed': 0,
+ 'ol::start': 0,
+ 'ol::type': 0,
+ 'optgroup::disabled': 0,
+ 'optgroup::label': 0,
+ 'option::disabled': 0,
+ 'option::label': 0,
+ 'option::selected': 0,
+ 'option::value': 0,
+ 'output::for': 6,
+ 'output::name': 8,
+ 'p::align': 0,
+ 'pre::width': 0,
+ 'progress::max': 0,
+ 'progress::min': 0,
+ 'progress::value': 0,
+ 'q::cite': 1,
+ 'select::autocomplete': 0,
+ 'select::disabled': 0,
+ 'select::multiple': 0,
+ 'select::name': 8,
+ 'select::onblur': 2,
+ 'select::onchange': 2,
+ 'select::onfocus': 2,
+ 'select::required': 0,
+ 'select::size': 0,
+ 'source::type': 0,
+ 'table::align': 0,
+ 'table::bgcolor': 0,
+ 'table::border': 0,
+ 'table::cellpadding': 0,
+ 'table::cellspacing': 0,
+ 'table::frame': 0,
+ 'table::rules': 0,
+ 'table::summary': 0,
+ 'table::width': 0,
+ 'tbody::align': 0,
+ 'tbody::char': 0,
+ 'tbody::charoff': 0,
+ 'tbody::valign': 0,
+ 'td::abbr': 0,
+ 'td::align': 0,
+ 'td::axis': 0,
+ 'td::bgcolor': 0,
+ 'td::char': 0,
+ 'td::charoff': 0,
+ 'td::colspan': 0,
+ 'td::headers': 6,
+ 'td::height': 0,
+ 'td::nowrap': 0,
+ 'td::rowspan': 0,
+ 'td::scope': 0,
+ 'td::valign': 0,
+ 'td::width': 0,
+ 'textarea::accesskey': 0,
+ 'textarea::autocomplete': 0,
+ 'textarea::cols': 0,
+ 'textarea::disabled': 0,
+ 'textarea::inputmode': 0,
+ 'textarea::name': 8,
+ 'textarea::onblur': 2,
+ 'textarea::onchange': 2,
+ 'textarea::onfocus': 2,
+ 'textarea::onselect': 2,
+ 'textarea::placeholder': 0,
+ 'textarea::readonly': 0,
+ 'textarea::required': 0,
+ 'textarea::rows': 0,
+ 'textarea::wrap': 0,
+ 'tfoot::align': 0,
+ 'tfoot::char': 0,
+ 'tfoot::charoff': 0,
+ 'tfoot::valign': 0,
+ 'th::abbr': 0,
+ 'th::align': 0,
+ 'th::axis': 0,
+ 'th::bgcolor': 0,
+ 'th::char': 0,
+ 'th::charoff': 0,
+ 'th::colspan': 0,
+ 'th::headers': 6,
+ 'th::height': 0,
+ 'th::nowrap': 0,
+ 'th::rowspan': 0,
+ 'th::scope': 0,
+ 'th::valign': 0,
+ 'th::width': 0,
+ 'thead::align': 0,
+ 'thead::char': 0,
+ 'thead::charoff': 0,
+ 'thead::valign': 0,
+ 'tr::align': 0,
+ 'tr::bgcolor': 0,
+ 'tr::char': 0,
+ 'tr::charoff': 0,
+ 'tr::valign': 0,
+ 'track::default': 0,
+ 'track::kind': 0,
+ 'track::label': 0,
+ 'track::srclang': 0,
+ 'ul::compact': 0,
+ 'ul::type': 0,
+ 'video::controls': 0,
+ 'video::height': 0,
+ 'video::loop': 0,
+ 'video::mediagroup': 5,
+ 'video::muted': 0,
+ 'video::poster': 1,
+ 'video::preload': 0,
+ 'video::src': 1,
+ 'video::width': 0
+};
+html4[ 'ATTRIBS' ] = html4.ATTRIBS;
+html4.eflags = {
+ 'OPTIONAL_ENDTAG': 1,
+ 'EMPTY': 2,
+ 'CDATA': 4,
+ 'RCDATA': 8,
+ 'UNSAFE': 16,
+ 'FOLDABLE': 32,
+ 'SCRIPT': 64,
+ 'STYLE': 128,
+ 'VIRTUALIZED': 256
+};
+html4[ 'eflags' ] = html4.eflags;
+html4.ELEMENTS = {
+ 'a': 0,
+ 'abbr': 0,
+ 'acronym': 0,
+ 'address': 0,
+ 'applet': 272,
+ 'area': 2,
+ 'article': 0,
+ 'aside': 0,
+ 'audio': 0,
+ 'b': 0,
+ 'base': 274,
+ 'basefont': 274,
+ 'bdi': 0,
+ 'bdo': 0,
+ 'big': 0,
+ 'blockquote': 0,
+ 'body': 305,
+ 'br': 2,
+ 'button': 0,
+ 'canvas': 0,
+ 'caption': 0,
+ 'center': 0,
+ 'cite': 0,
+ 'code': 0,
+ 'col': 2,
+ 'colgroup': 1,
+ 'command': 2,
+ 'data': 0,
+ 'datalist': 0,
+ 'dd': 1,
+ 'del': 0,
+ 'details': 0,
+ 'dfn': 0,
+ 'dialog': 272,
+ 'dir': 0,
+ 'div': 0,
+ 'dl': 0,
+ 'dt': 1,
+ 'em': 0,
+ 'fieldset': 0,
+ 'figcaption': 0,
+ 'figure': 0,
+ 'font': 0,
+ 'footer': 0,
+ 'form': 0,
+ 'frame': 274,
+ 'frameset': 272,
+ 'h1': 0,
+ 'h2': 0,
+ 'h3': 0,
+ 'h4': 0,
+ 'h5': 0,
+ 'h6': 0,
+ 'head': 305,
+ 'header': 0,
+ 'hgroup': 0,
+ 'hr': 2,
+ 'html': 305,
+ 'i': 0,
+ 'iframe': 4,
+ 'img': 2,
+ 'input': 2,
+ 'ins': 0,
+ 'isindex': 274,
+ 'kbd': 0,
+ 'keygen': 274,
+ 'label': 0,
+ 'legend': 0,
+ 'li': 1,
+ 'link': 274,
+ 'map': 0,
+ 'mark': 0,
+ 'menu': 0,
+ 'meta': 274,
+ 'meter': 0,
+ 'nav': 0,
+ 'nobr': 0,
+ 'noembed': 276,
+ 'noframes': 276,
+ 'noscript': 276,
+ 'object': 272,
+ 'ol': 0,
+ 'optgroup': 0,
+ 'option': 1,
+ 'output': 0,
+ 'p': 1,
+ 'param': 274,
+ 'pre': 0,
+ 'progress': 0,
+ 'q': 0,
+ 's': 0,
+ 'samp': 0,
+ 'script': 84,
+ 'section': 0,
+ 'select': 0,
+ 'small': 0,
+ 'source': 2,
+ 'span': 0,
+ 'strike': 0,
+ 'strong': 0,
+ 'style': 148,
+ 'sub': 0,
+ 'summary': 0,
+ 'sup': 0,
+ 'table': 0,
+ 'tbody': 1,
+ 'td': 1,
+ 'textarea': 8,
+ 'tfoot': 1,
+ 'th': 1,
+ 'thead': 1,
+ 'time': 0,
+ 'title': 280,
+ 'tr': 1,
+ 'track': 2,
+ 'tt': 0,
+ 'u': 0,
+ 'ul': 0,
+ 'var': 0,
+ 'video': 0,
+ 'wbr': 2
+};
+html4[ 'ELEMENTS' ] = html4.ELEMENTS;
+html4.ELEMENT_DOM_INTERFACES = {
+ 'a': 'HTMLAnchorElement',
+ 'abbr': 'HTMLElement',
+ 'acronym': 'HTMLElement',
+ 'address': 'HTMLElement',
+ 'applet': 'HTMLAppletElement',
+ 'area': 'HTMLAreaElement',
+ 'article': 'HTMLElement',
+ 'aside': 'HTMLElement',
+ 'audio': 'HTMLAudioElement',
+ 'b': 'HTMLElement',
+ 'base': 'HTMLBaseElement',
+ 'basefont': 'HTMLBaseFontElement',
+ 'bdi': 'HTMLElement',
+ 'bdo': 'HTMLElement',
+ 'big': 'HTMLElement',
+ 'blockquote': 'HTMLQuoteElement',
+ 'body': 'HTMLBodyElement',
+ 'br': 'HTMLBRElement',
+ 'button': 'HTMLButtonElement',
+ 'canvas': 'HTMLCanvasElement',
+ 'caption': 'HTMLTableCaptionElement',
+ 'center': 'HTMLElement',
+ 'cite': 'HTMLElement',
+ 'code': 'HTMLElement',
+ 'col': 'HTMLTableColElement',
+ 'colgroup': 'HTMLTableColElement',
+ 'command': 'HTMLCommandElement',
+ 'data': 'HTMLElement',
+ 'datalist': 'HTMLDataListElement',
+ 'dd': 'HTMLElement',
+ 'del': 'HTMLModElement',
+ 'details': 'HTMLDetailsElement',
+ 'dfn': 'HTMLElement',
+ 'dialog': 'HTMLDialogElement',
+ 'dir': 'HTMLDirectoryElement',
+ 'div': 'HTMLDivElement',
+ 'dl': 'HTMLDListElement',
+ 'dt': 'HTMLElement',
+ 'em': 'HTMLElement',
+ 'fieldset': 'HTMLFieldSetElement',
+ 'figcaption': 'HTMLElement',
+ 'figure': 'HTMLElement',
+ 'font': 'HTMLFontElement',
+ 'footer': 'HTMLElement',
+ 'form': 'HTMLFormElement',
+ 'frame': 'HTMLFrameElement',
+ 'frameset': 'HTMLFrameSetElement',
+ 'h1': 'HTMLHeadingElement',
+ 'h2': 'HTMLHeadingElement',
+ 'h3': 'HTMLHeadingElement',
+ 'h4': 'HTMLHeadingElement',
+ 'h5': 'HTMLHeadingElement',
+ 'h6': 'HTMLHeadingElement',
+ 'head': 'HTMLHeadElement',
+ 'header': 'HTMLElement',
+ 'hgroup': 'HTMLElement',
+ 'hr': 'HTMLHRElement',
+ 'html': 'HTMLHtmlElement',
+ 'i': 'HTMLElement',
+ 'iframe': 'HTMLIFrameElement',
+ 'img': 'HTMLImageElement',
+ 'input': 'HTMLInputElement',
+ 'ins': 'HTMLModElement',
+ 'isindex': 'HTMLUnknownElement',
+ 'kbd': 'HTMLElement',
+ 'keygen': 'HTMLKeygenElement',
+ 'label': 'HTMLLabelElement',
+ 'legend': 'HTMLLegendElement',
+ 'li': 'HTMLLIElement',
+ 'link': 'HTMLLinkElement',
+ 'map': 'HTMLMapElement',
+ 'mark': 'HTMLElement',
+ 'menu': 'HTMLMenuElement',
+ 'meta': 'HTMLMetaElement',
+ 'meter': 'HTMLMeterElement',
+ 'nav': 'HTMLElement',
+ 'nobr': 'HTMLElement',
+ 'noembed': 'HTMLElement',
+ 'noframes': 'HTMLElement',
+ 'noscript': 'HTMLElement',
+ 'object': 'HTMLObjectElement',
+ 'ol': 'HTMLOListElement',
+ 'optgroup': 'HTMLOptGroupElement',
+ 'option': 'HTMLOptionElement',
+ 'output': 'HTMLOutputElement',
+ 'p': 'HTMLParagraphElement',
+ 'param': 'HTMLParamElement',
+ 'pre': 'HTMLPreElement',
+ 'progress': 'HTMLProgressElement',
+ 'q': 'HTMLQuoteElement',
+ 's': 'HTMLElement',
+ 'samp': 'HTMLElement',
+ 'script': 'HTMLScriptElement',
+ 'section': 'HTMLElement',
+ 'select': 'HTMLSelectElement',
+ 'small': 'HTMLElement',
+ 'source': 'HTMLSourceElement',
+ 'span': 'HTMLSpanElement',
+ 'strike': 'HTMLElement',
+ 'strong': 'HTMLElement',
+ 'style': 'HTMLStyleElement',
+ 'sub': 'HTMLElement',
+ 'summary': 'HTMLElement',
+ 'sup': 'HTMLElement',
+ 'table': 'HTMLTableElement',
+ 'tbody': 'HTMLTableSectionElement',
+ 'td': 'HTMLTableDataCellElement',
+ 'textarea': 'HTMLTextAreaElement',
+ 'tfoot': 'HTMLTableSectionElement',
+ 'th': 'HTMLTableHeaderCellElement',
+ 'thead': 'HTMLTableSectionElement',
+ 'time': 'HTMLTimeElement',
+ 'title': 'HTMLTitleElement',
+ 'tr': 'HTMLTableRowElement',
+ 'track': 'HTMLTrackElement',
+ 'tt': 'HTMLElement',
+ 'u': 'HTMLElement',
+ 'ul': 'HTMLUListElement',
+ 'var': 'HTMLElement',
+ 'video': 'HTMLVideoElement',
+ 'wbr': 'HTMLElement'
+};
+html4[ 'ELEMENT_DOM_INTERFACES' ] = html4.ELEMENT_DOM_INTERFACES;
+html4.ueffects = {
+ 'NOT_LOADED': 0,
+ 'SAME_DOCUMENT': 1,
+ 'NEW_DOCUMENT': 2
+};
+html4[ 'ueffects' ] = html4.ueffects;
+html4.URIEFFECTS = {
+ 'a::href': 2,
+ 'area::href': 2,
+ 'audio::src': 1,
+ 'blockquote::cite': 0,
+ 'command::icon': 1,
+ 'del::cite': 0,
+ 'form::action': 2,
+ 'img::src': 1,
+ 'input::src': 1,
+ 'ins::cite': 0,
+ 'q::cite': 0,
+ 'video::poster': 1,
+ 'video::src': 1
+};
+html4[ 'URIEFFECTS' ] = html4.URIEFFECTS;
+html4.ltypes = {
+ 'UNSANDBOXED': 2,
+ 'SANDBOXED': 1,
+ 'DATA': 0
+};
+html4[ 'ltypes' ] = html4.ltypes;
+html4.LOADERTYPES = {
+ 'a::href': 2,
+ 'area::href': 2,
+ 'audio::src': 2,
+ 'blockquote::cite': 2,
+ 'command::icon': 1,
+ 'del::cite': 2,
+ 'form::action': 2,
+ 'img::src': 1,
+ 'input::src': 1,
+ 'ins::cite': 2,
+ 'q::cite': 2,
+ 'video::poster': 1,
+ 'video::src': 2
+};
+html4[ 'LOADERTYPES' ] = html4.LOADERTYPES;
+
+return html4
+});
diff --git a/web-ui/app/js/mail_list/domain/refresher.js b/web-ui/app/js/mail_list/domain/refresher.js
new file mode 100644
index 00000000..38c9cde5
--- /dev/null
+++ b/web-ui/app/js/mail_list/domain/refresher.js
@@ -0,0 +1,43 @@
+/*
+ * 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';
+
+ return defineComponent(refresher);
+
+ function refresher() {
+ this.defaultAttrs({
+ interval: 20000
+ });
+
+ this.setupRefresher = function() {
+ setTimeout(this.doRefresh.bind(this), this.attr.interval);
+ };
+
+ this.doRefresh = function() {
+ this.trigger(document, events.ui.mails.refresh);
+ this.setupRefresher();
+ };
+
+ this.after('initialize', function () {
+ if (features.isAutoRefreshEnabled()) {
+ this.setupRefresher();
+ }
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list/ui/mail_item_factory.js b/web-ui/app/js/mail_list/ui/mail_item_factory.js
new file mode 100644
index 00000000..7205d35c
--- /dev/null
+++ b/web-ui/app/js/mail_list/ui/mail_item_factory.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(
+ [
+ 'mail_list/ui/mail_items/generic_mail_item',
+ 'mail_list/ui/mail_items/draft_item',
+ 'mail_list/ui/mail_items/sent_item'
+ ],
+ function (GenericMailItem, DraftItem, SentItem) {
+ 'use strict';
+
+ var MAIL_ITEM_TYPE = {
+ 'drafts': DraftItem,
+ 'sent': SentItem,
+ 'trash': GenericMailItem
+ };
+
+ var TEMPLATE_TYPE = {
+ 'drafts': 'draft',
+ 'sent': 'sent',
+ 'trash': 'trash'
+ };
+
+ var createAndAttach = function (nodeToAttachTo, mail, currentMailIdent, currentTag, isChecked) {
+ var mailItemContainer = $('<li>', { id: 'mail-' + mail.ident});
+ nodeToAttachTo.append(mailItemContainer);
+
+ mail.currentTag = currentTag;
+ var mailToCreate = MAIL_ITEM_TYPE[mail.mailbox] || GenericMailItem;
+ mailToCreate.attachTo(mailItemContainer, {
+ mail: mail,
+ selected: mail.ident === currentMailIdent,
+ tag: currentTag,
+ isChecked: isChecked,
+ templateType: TEMPLATE_TYPE[mail.mailbox] || 'single'
+ });
+
+ };
+
+ return {
+ createAndAttach: createAndAttach
+ };
+ }
+);
diff --git a/web-ui/app/js/mail_list/ui/mail_items/draft_item.js b/web-ui/app/js/mail_list/ui/mail_items/draft_item.js
new file mode 100644
index 00000000..57fbafd5
--- /dev/null
+++ b/web-ui/app/js/mail_list/ui/mail_items/draft_item.js
@@ -0,0 +1,55 @@
+/*
+ * 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',
+ 'mail_list/ui/mail_items/mail_item',
+ 'page/events'
+ ],
+
+ function (defineComponent, viewHelpers, mailItem, events) {
+ 'use strict';
+
+ return defineComponent(draftItem, mailItem);
+
+ function draftItem() {
+ this.triggerOpenMail = function (ev) {
+ if (this.isOpeningOnANewTab(ev)) {
+ return;
+ }
+ this.trigger(document, events.dispatchers.rightPane.openDraft, { ident: this.attr.mail.ident });
+ this.trigger(document, events.ui.mail.updateSelected, { ident: this.attr.mail.ident });
+ this.trigger(document, events.router.pushState, { mailIdent: this.attr.mail.ident });
+ ev.preventDefault(); // don't let the hashchange trigger a popstate
+ };
+
+ this.after('initialize', function () {
+ this.render();
+
+ if (this.attr.isChecked) {
+ this.checkCheckbox();
+ }
+
+ this.on(document, events.ui.composeBox.newMessage, this.doUnselect);
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.mails.teardown, this.teardown);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.js b/web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.js
new file mode 100644
index 00000000..939f7e1b
--- /dev/null
+++ b/web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.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',
+ 'mail_list/ui/mail_items/mail_item',
+ 'page/events'
+ ],
+
+ function (defineComponent, viewHelpers, mailItem, events) {
+ 'use strict';
+
+ return defineComponent(genericMailItem, mailItem);
+
+ function genericMailItem() {
+ this.status = {
+ READ: 'read'
+ };
+
+ this.triggerOpenMail = function (ev) {
+ if (this.isOpeningOnANewTab(ev)) {
+ updateMailStatusToRead.call(this);
+ return;
+ }
+ this.trigger(document, events.ui.mail.open, { ident: this.attr.mail.ident });
+ this.trigger(document, events.router.pushState, { mailIdent: this.attr.mail.ident });
+ ev.preventDefault(); // don't let the hashchange trigger a popstate
+ };
+
+ function updateMailStatusToRead() {
+ if (!_.contains(this.attr.mail.status, this.status.READ)) {
+ var mail_read_data = { ident: this.attr.mail.ident, tags: this.attr.mail.tags, mailbox: this.attr.mail.mailbox };
+ this.trigger(document, events.mail.read, mail_read_data);
+ this.attr.mail.status.push(this.status.READ);
+ this.$node.addClass(viewHelpers.formatStatusClasses(this.attr.mail.status));
+ }
+ }
+
+ this.openMail = function (ev, data) {
+ if (data.ident !== this.attr.mail.ident) {
+ return;
+ }
+ updateMailStatusToRead.call(this);
+
+ this.trigger(document, events.ui.mail.updateSelected, { ident: this.attr.mail.ident });
+ };
+
+ this.updateTags = function(ev, data) {
+ if(data.ident === this.attr.mail.ident){
+ this.attr.tags = data.tags;
+ if(!_.contains(this.attr.tags, this.attr.tag)) {
+ this.teardown();
+ } else {
+ this.render();
+ }
+ }
+ };
+
+ this.deleteMail = function(ev, data) {
+ if(data.mail.ident === this.attr.mail.ident){
+ this.teardown();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.render();
+
+ if (this.attr.isChecked) {
+ this.checkCheckbox();
+ }
+
+ this.on(document, events.ui.composeBox.newMessage, this.doUnselect);
+ this.on(document, events.ui.mail.open, this.openMail);
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.mails.teardown, this.teardown);
+ this.on(document, events.mail.tags.update, this.updateTags);
+ this.on(document, events.mail.delete, this.deleteMail);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list/ui/mail_items/mail_item.js b/web-ui/app/js/mail_list/ui/mail_items/mail_item.js
new file mode 100644
index 00000000..be664289
--- /dev/null
+++ b/web-ui/app/js/mail_list/ui/mail_items/mail_item.js
@@ -0,0 +1,88 @@
+/*
+ * 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/view_helper',
+ 'views/templates',
+ 'page/events'
+ ],
+ function (viewHelper, templates, events) {
+
+ 'use strict';
+ function mailItem() {
+ this.updateSelected = function (ev, data) {
+ if (data.ident === this.attr.mail.ident) { this.doSelect(); }
+ else { this.doUnselect(); }
+ };
+
+ this.isOpeningOnANewTab = function (ev) {
+ return ev.metaKey || ev.ctrlKey || ev.which === 2;
+ };
+
+ this.doSelect = function () {
+ this.$node.addClass('selected');
+ };
+
+ this.doUnselect = function () {
+ this.$node.removeClass('selected');
+ };
+
+ this.doMailChecked = function (ev) {
+ if (ev.target.checked) {
+ this.checkCheckbox();
+ } else {
+ this.uncheckCheckbox();
+ }
+ };
+
+ this.checkboxElement = function () {
+ return this.$node.find('input[type=checkbox]');
+ };
+
+ this.checkCheckbox = function () {
+ this.checkboxElement().prop('checked', true);
+ this.trigger(document, events.ui.mail.checked, { mail: this.attr.mail});
+ };
+
+ this.uncheckCheckbox = function () {
+ this.checkboxElement().prop('checked', false);
+ this.trigger(document, events.ui.mail.unchecked, { mail: this.attr.mail});
+ };
+
+ this.render = function () {
+ this.attr.mail.tagsForListView = _.without(this.attr.mail.tags, this.attr.tag);
+ var mailItemHtml = templates.mails[this.attr.templateType](this.attr.mail);
+ this.$node.html(mailItemHtml);
+ this.$node.addClass("mail-list-entry");
+ this.$node.addClass(viewHelper.formatStatusClasses(this.attr.mail.status));
+ if (this.attr.selected) { this.doSelect(); }
+ this.on(this.$node.find('a'), 'click', this.triggerOpenMail);
+ };
+
+ this.after('initialize', function () {
+ this.on(this.$node.find('input[type=checkbox]'), 'change', this.doMailChecked);
+ this.on(document, events.ui.mails.cleanSelected, this.doUnselect);
+ this.on(document, events.ui.tag.select, this.doUnselect);
+ this.on(document, events.ui.tag.select, this.uncheckCheckbox);
+ this.on(document, events.ui.mails.uncheckAll, this.uncheckCheckbox);
+ this.on(document, events.ui.mails.checkAll, this.checkCheckbox);
+ });
+ }
+
+ return mailItem;
+});
diff --git a/web-ui/app/js/mail_list/ui/mail_items/sent_item.js b/web-ui/app/js/mail_list/ui/mail_items/sent_item.js
new file mode 100644
index 00000000..9e511068
--- /dev/null
+++ b/web-ui/app/js/mail_list/ui/mail_items/sent_item.js
@@ -0,0 +1,61 @@
+/*
+ * 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',
+ 'mail_list/ui/mail_items/mail_item',
+ 'page/events'
+ ],
+
+ function (defineComponent, mailItem, events) {
+ 'use strict';
+
+ return defineComponent(sentItem, mailItem);
+
+ function sentItem() {
+ this.triggerOpenMail = function (ev) {
+ if (this.isOpeningOnANewTab(ev)) {
+ return;
+ }
+ this.trigger(document, events.ui.mail.open, { ident: this.attr.mail.ident });
+ this.trigger(document, events.router.pushState, { mailIdent: this.attr.mail.ident });
+ ev.preventDefault(); // don't let the hashchange trigger a popstate
+ };
+
+ this.openMail = function (ev, data) {
+ if (data.ident !== this.attr.mail.ident) {
+ return;
+ }
+ this.trigger(document, events.ui.mail.updateSelected, { ident: this.attr.mail.ident });
+ };
+
+ this.after('initialize', function () {
+ this.render();
+
+ if (this.attr.isChecked) {
+ this.checkCheckbox();
+ }
+
+ this.on(document, events.ui.composeBox.newMessage, this.doUnselect);
+ this.on(document, events.ui.mail.open, this.openMail);
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.mails.teardown, this.teardown);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list/ui/mail_list.js b/web-ui/app/js/mail_list/ui/mail_list.js
new file mode 100644
index 00000000..af4821a8
--- /dev/null
+++ b/web-ui/app/js/mail_list/ui/mail_list.js
@@ -0,0 +1,182 @@
+/*
+ * 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',
+ 'mail_list/ui/mail_item_factory',
+ 'page/router/url_params',
+ 'page/events'
+ ],
+
+ function (defineComponent, utils, MailItemFactory, urlParams, events) {
+ 'use strict';
+
+ return defineComponent(mailList);
+
+ function mailList () {
+ var openMailEventFor = function (tag) {
+ return tag === 'drafts' ? events.dispatchers.rightPane.openDraft : events.ui.mail.open;
+ };
+
+ this.defaultAttrs({
+ mail: '.mail',
+ currentMailIdent: '',
+ urlParams: urlParams,
+ initialized: false,
+ checkedMails: {}
+ });
+
+ this.appendMail = function (mail) {
+ var isChecked = mail.ident in this.attr.checkedMails;
+ MailItemFactory.createAndAttach(this.$node, mail, this.attr.currentMailIdent, this.attr.currentTag, isChecked);
+ };
+
+ this.resetMailList = function () {
+ this.trigger(document, events.mails.teardown);
+ this.$node.empty();
+ };
+
+ this.triggerMailOpenForPopState = function (data) {
+ if (data.mailIdent) {
+ this.trigger(document, openMailEventFor(data.tag), { ident: data.mailIdent });
+ }
+ };
+
+ this.shouldSelectEmailFromUrlMailIdent = function () {
+ return this.attr.urlParams.hasMailIdent();
+ };
+
+ this.selectMailBasedOnUrlMailIdent = function () {
+ var mailIdent = this.attr.urlParams.getMailIdent();
+ this.trigger(document, openMailEventFor(this.attr.currentTag), { ident: mailIdent });
+ this.trigger(document, events.router.pushState, { tag: this.attr.currentTag, mailIdent: mailIdent });
+ };
+
+ this.updateCurrentTagAndMail = function (data) {
+ if (data.ident) {
+ this.attr.currentMailIdent = data.ident;
+ }
+
+ this.attr.currentTag = data.tag || this.attr.currentTag;
+
+ this.updateCheckAllCheckbox();
+ };
+
+ this.renderMails = function (mails) {
+ _.each(mails, this.appendMail, this);
+ this.trigger(document, events.search.highlightResults, {where: '#mail-list'});
+ this.trigger(document, events.search.highlightResults, {where: '.mail-read-view__header'});
+ };
+
+ this.triggerScrollReset = function () {
+ this.trigger(document, events.dispatchers.middlePane.resetScroll);
+ };
+
+ this.showMails = function (event, data) {
+ this.updateCurrentTagAndMail(data);
+ this.refreshMailList(null, data);
+ this.triggerMailOpenForPopState(data);
+ this.openMailFromUrl();
+ };
+
+ this.refreshMailList = function (ev, data) {
+ if (ev) { // triggered by the event, so we need to refresh the tag list
+ this.trigger(document, events.dispatchers.tags.refreshTagList, { skipMailListRefresh: true });
+ }
+ this.resetMailList();
+ this.renderMails(data.mails);
+ };
+
+ this.updateSelected = function (ev, data) {
+ if (data.ident !== this.attr.currentMailIdent) {
+ this.attr.currentMailIdent = data.ident;
+ }
+ };
+
+ this.cleanSelected = function () {
+ this.attr.currentMailIdent = '';
+ this.triggerScrollReset();
+ };
+
+ this.respondWithCheckedMails = function (ev, caller) {
+ this.trigger(caller, events.ui.mail.hereChecked, {checkedMails: this.attr.checkedMails});
+ };
+
+ this.updateCheckAllCheckbox = function () {
+ this.trigger(document, events.ui.mails.hasMailsChecked, _.keys(this.attr.checkedMails).length > 0);
+ };
+
+ this.addToCheckedMails = function (ev, data) {
+ this.attr.checkedMails[data.mail.ident] = data.mail;
+ this.updateCheckAllCheckbox();
+ };
+
+ this.removeFromCheckedMails = function (ev, data) {
+ if (data.mails) {
+ _.each(data.mails, function (mail) {
+ delete this.attr.checkedMails[mail.ident];
+ }, this);
+ } else {
+ delete this.attr.checkedMails[data.mail.ident];
+ }
+ this.updateCheckAllCheckbox();
+ };
+
+ this.refreshWithScroll = function () {
+ this.trigger(document, events.ui.mails.refresh);
+ this.triggerScrollReset();
+ };
+
+ this.refreshAfterSaveDraft = function () {
+ if (this.attr.currentTag === 'drafts') {
+ this.refreshWithScroll();
+ }
+ };
+
+ this.refreshAfterMailSent = function () {
+ if (this.attr.currentTag === 'drafts' || this.attr.currentTag === 'sent') {
+ this.refreshWithScroll();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mails.cleanSelected, this.cleanSelected);
+ this.on(document, events.ui.tag.select, this.cleanSelected);
+
+ this.on(document, events.mails.available, this.showMails);
+ this.on(document, events.mails.availableForRefresh, this.refreshMailList);
+
+ this.on(document, events.mail.draftSaved, this.refreshAfterSaveDraft);
+ this.on(document, events.mail.sent, this.refreshAfterMailSent);
+
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.ui.mail.wantChecked, this.respondWithCheckedMails);
+ this.on(document, events.ui.mail.checked, this.addToCheckedMails);
+ this.on(document, events.ui.mail.unchecked, this.removeFromCheckedMails);
+
+ this.openMailFromUrl = utils.once(function () {
+ if (this.shouldSelectEmailFromUrlMailIdent()) {
+ this.selectMailBasedOnUrlMailIdent();
+ }
+ });
+
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/archive_many_trigger.js b/web-ui/app/js/mail_list_actions/ui/archive_many_trigger.js
new file mode 100644
index 00000000..b148cdce
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/archive_many_trigger.js
@@ -0,0 +1,29 @@
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(definecomponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return definecomponent(archiveManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+ function archiveManyTrigger() {
+
+ this.getMailsToArchive = function() {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.archiveManyEmails = function(event, data) {
+ this.trigger(document, events.mail.archiveMany, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToArchive);
+ this.on(events.ui.mail.hereChecked, this.archiveManyEmails);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/compose_trigger.js b/web-ui/app/js/mail_list_actions/ui/compose_trigger.js
new file mode 100644
index 00000000..ec79cb26
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/compose_trigger.js
@@ -0,0 +1,57 @@
+/*
+ * 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(composeTrigger);
+
+ function composeTrigger() {
+
+ this.defaultAttrs({});
+
+ this.render = function() {
+ this.$node.html(templates.mailActions.composeTrigger);
+ };
+
+ this.enableComposing = function(event, data) {
+ this.trigger(document, events.dispatchers.rightPane.openComposeBox);
+ };
+
+ this.showEmailSuccess = function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Your message was sent!', class: 'success'});
+ };
+
+ this.showEmailError = function (ev, data) {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Error, message not sent: ' + data.responseJSON.message, class: 'error'});
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on('click', this.enableComposing);
+ this.on(document, events.mail.sent, this.showEmailSuccess);
+ this.on(document, events.mail.send_failed, this.showEmailError);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js b/web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js
new file mode 100644
index 00000000..dd2f67a5
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * 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_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(deleteManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function deleteManyTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToDelete = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.deleteManyEmails = function (event, data) {
+ this.trigger(document, events.ui.mail.deleteMany, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToDelete);
+ this.on(events.ui.mail.hereChecked, this.deleteManyEmails);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js b/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js
new file mode 100644
index 00000000..69e5fde4
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js
@@ -0,0 +1,93 @@
+/*
+ * 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',
+ 'page/router/url_params',
+ 'mail_list_actions/ui/compose_trigger',
+ 'mail_list_actions/ui/refresh_trigger',
+ 'mail_list/domain/refresher',
+ 'mail_list_actions/ui/toggle_check_all_trigger',
+ 'mail_list_actions/ui/pagination_trigger',
+ 'mail_list_actions/ui/delete_many_trigger',
+ 'mail_list_actions/ui/recover_many_trigger',
+ 'mail_list_actions/ui/archive_many_trigger',
+ 'mail_list_actions/ui/mark_many_as_read_trigger',
+ 'mail_list_actions/ui/mark_as_unread_trigger'
+ ],
+
+ function (
+ defineComponent,
+ templates,
+ events,
+ urlParams,
+ composeTrigger,
+ refreshTrigger,
+ refresher,
+ toggleCheckAllMailTrigger,
+ paginationTrigger,
+ deleteManyTrigger,
+ recoverManyTrigger,
+ archiveManyTrigger,
+ markManyAsReadTrigger,
+ markAsUnreadTrigger
+ ) {
+ 'use strict';
+ return defineComponent(mailsActions);
+
+ function mailsActions() {
+ this.render = function() {
+ this.$node.html(this.getActionsBoxTemplate());
+ refreshTrigger.attachTo('#refresh-trigger');
+ composeTrigger.attachTo('#compose-trigger');
+ toggleCheckAllMailTrigger.attachTo('#toggle-check-all-emails');
+ paginationTrigger.attachTo('#pagination-trigger');
+ deleteManyTrigger.attachTo('#delete-selected');
+ recoverManyTrigger.attachTo('#recover-selected');
+ archiveManyTrigger.attachTo('#archive-selected');
+ markManyAsReadTrigger.attachTo('#mark-selected-as-read');
+ markAsUnreadTrigger.attachTo('#mark-selected-as-unread');
+ refresher.attachTo(document);
+ };
+
+ this.getCurrentTag = function () {
+ return this.attr.currentTag || urlParams.getTag();
+ };
+
+ this.updateCurrentTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ this.render();
+ };
+
+ this.getActionsBoxTemplate = function () {
+ if(this.getCurrentTag() === 'trash') {
+ return templates.mailActions.trashActionsBox();
+ } else {
+ return templates.mailActions.actionsBox();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.tag.select, this.updateCurrentTag);
+ this.render();
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js b/web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js
new file mode 100644
index 00000000..2584e453
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * 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_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(markAsUnreadTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function markAsUnreadTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToMarkAsUnread = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.markManyEmailsAsUnread = function (event, data) {
+ this.trigger(document, events.mail.unread, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToMarkAsUnread);
+ this.on(events.ui.mail.hereChecked, this.markManyEmailsAsUnread);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js b/web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js
new file mode 100644
index 00000000..c16a2229
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * 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_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(markManyAsReadTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function markManyAsReadTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToMarkAsRead = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.markManyEmailsAsRead = function (event, data) {
+ this.trigger(document, events.mail.read, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToMarkAsRead);
+ this.on(events.ui.mail.hereChecked, this.markManyEmailsAsRead);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/pagination_trigger.js b/web-ui/app/js/mail_list_actions/ui/pagination_trigger.js
new file mode 100644
index 00000000..3bc13d40
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/pagination_trigger.js
@@ -0,0 +1,66 @@
+/*
+ * 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(paginationTrigger);
+
+ function paginationTrigger() {
+ this.defaultAttrs({
+ previous: '#left-arrow',
+ next: '#right-arrow',
+ currentPage: '#current-page'
+ });
+
+ this.renderWithPageNumber = function(pageNumber) {
+ this.$node.html(templates.mailActions.paginationTrigger({
+ currentPage: pageNumber
+ }));
+ this.on(this.attr.previous, 'click', this.previousPage);
+ this.on(this.attr.next, 'click', this.nextPage);
+ };
+
+ this.render = function() {
+ this.renderWithPageNumber(1);
+ };
+
+ this.updatePageDisplay = function(event, data) {
+ this.renderWithPageNumber(data.currentPage);
+ };
+
+ this.previousPage = function(event) {
+ this.trigger(document, events.ui.page.previous);
+ };
+
+ this.nextPage = function(event) {
+ this.trigger(document, events.ui.page.next);
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on(document, events.ui.page.changed, this.updatePageDisplay);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js b/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js
new file mode 100644
index 00000000..e0a32094
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * 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_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(recoverManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function recoverManyTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToRecover = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.recoverManyEmails = function (event, data) {
+ this.trigger(document, events.ui.mail.recoverMany, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToRecover);
+ this.on(events.ui.mail.hereChecked, this.recoverManyEmails);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/refresh_trigger.js b/web-ui/app/js/mail_list_actions/ui/refresh_trigger.js
new file mode 100644
index 00000000..a16270d2
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/refresh_trigger.js
@@ -0,0 +1,44 @@
+/*
+ * 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(refreshTrigger);
+
+ function refreshTrigger() {
+ this.render = function() {
+ this.$node.html(templates.mailActions.refreshTrigger);
+ };
+
+ this.refresh = function(event) {
+ this.trigger(document, events.ui.mails.refresh);
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on('click', this.refresh);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js b/web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js
new file mode 100644
index 00000000..71c65346
--- /dev/null
+++ b/web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js
@@ -0,0 +1,49 @@
+/*
+ * 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'
+ ],
+
+ function(defineComponent, events) {
+ 'use strict';
+
+ return defineComponent(toggleCheckAllEmailsTrigger);
+
+ function toggleCheckAllEmailsTrigger() {
+ this.defaultAttrs({ });
+
+ this.toggleCheckAll = function(event) {
+ if (this.$node.prop('checked')) {
+ this.trigger(document, events.ui.mails.checkAll);
+ } else {
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }
+ };
+
+ this.setCheckbox = function (event, state) {
+ this.$node.prop('checked', state);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.toggleCheckAll);
+ this.on(document, events.ui.mails.hasMailsChecked, this.setCheckbox);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/mail_view/data/feedback_sender.js b/web-ui/app/js/mail_view/data/feedback_sender.js
new file mode 100644
index 00000000..2232dbe4
--- /dev/null
+++ b/web-ui/app/js/mail_view/data/feedback_sender.js
@@ -0,0 +1,49 @@
+/*
+ * 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',
+ 'helpers/monitored_ajax',
+ 'page/events'
+ ],
+ function (defineComponent, monitoredAjax, events) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ feedbackResource: '/feedback'
+ });
+
+ this.successSubmittingFeedback = function() {
+ this.trigger(document, events.feedback.submitted);
+ };
+
+ this.submitFeedback = function(event, data) {
+ monitoredAjax.call(_, this, this.attr.feedbackResource, {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data)
+ }).done(this.successSubmittingFeedback());
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.feedback.submit, this.submitFeedback);
+ });
+
+ });
+});
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..7a478dd8
--- /dev/null
+++ b/web-ui/app/js/mail_view/data/mail_builder.js
@@ -0,0 +1,102 @@
+/*
+ * 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(['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: '',
+ attachments: [],
+ 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;
+ },
+
+ attachment: function (attachmentList) {
+ mail.attachments = attachmentList;
+ 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..8bb01f70
--- /dev/null
+++ b/web-ui/app/js/mail_view/data/mail_sender.js
@@ -0,0 +1,93 @@
+/*
+ * 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',
+ 'mail_view/data/mail_builder',
+ 'page/events',
+ 'helpers/monitored_ajax',
+ 'features'
+ ],
+ function (defineComponent, mailBuilder, events, monitoredAjax, features) {
+ 'use strict';
+
+ return defineComponent(mailSender);
+
+ function mailSender() {
+ function successSendingMail(on){
+ return function(result) {
+ on.trigger(document, events.mail.sent, result);
+ };
+ }
+
+ function failureSendingMail(on) {
+ return function(result) {
+ on.trigger(document, events.mail.send_failed, result);
+ };
+ }
+
+ function successSaveDraft(on){
+ return function(result){
+ on.trigger(document, events.mail.draftSaved, result);
+ };
+ }
+
+ this.defaultAttrs({
+ mailsResource: '/mails'
+ });
+
+ this.sendMail = function(event, data) {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ monitoredAjax.call(_, this, this.attr.mailsResource, {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data)
+ }).done(successSendingMail(this)).fail(failureSendingMail(this));
+
+ };
+
+ this.saveMail = function(mail) {
+ return monitoredAjax.call(_, this, this.attr.mailsResource, {
+ type: 'PUT',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(mail),
+ skipErrorMessage: true
+ });
+ };
+
+ this.saveDraft = function(event, data) {
+ this.saveMail(data)
+ .done(successSaveDraft(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);
+ if(features.isEnabled('saveDraft')) {
+ 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/attachment_icon.js b/web-ui/app/js/mail_view/ui/attachment_icon.js
new file mode 100644
index 00000000..e04fc02a
--- /dev/null
+++ b/web-ui/app/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/app/js/mail_view/ui/attachment_list.js b/web-ui/app/js/mail_view/ui/attachment_list.js
new file mode 100644
index 00000000..4ef64960
--- /dev/null
+++ b/web-ui/app/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/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..101dc939
--- /dev/null
+++ b/web-ui/app/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/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..afe31914
--- /dev/null
+++ b/web-ui/app/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/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..47751d91
--- /dev/null
+++ b/web-ui/app/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/app/js/mail_view/ui/feedback_box.js b/web-ui/app/js/mail_view/ui/feedback_box.js
new file mode 100644
index 00000000..4e00ece8
--- /dev/null
+++ b/web-ui/app/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/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..a34bd55d
--- /dev/null
+++ b/web-ui/app/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/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..65cd0aaa
--- /dev/null
+++ b/web-ui/app/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/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..3408c8af
--- /dev/null
+++ b/web-ui/app/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/app/js/mail_view/ui/no_mails_available_pane.js b/web-ui/app/js/mail_view/ui/no_mails_available_pane.js
new file mode 100644
index 00000000..c62c6b30
--- /dev/null
+++ b/web-ui/app/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/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..a5fc2393
--- /dev/null
+++ b/web-ui/app/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/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..c13a52b1
--- /dev/null
+++ b/web-ui/app/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/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..2caa8d14
--- /dev/null
+++ b/web-ui/app/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/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..8a9c4eaf
--- /dev/null
+++ b/web-ui/app/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/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..624ac4f5
--- /dev/null
+++ b/web-ui/app/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/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..a174d185
--- /dev/null
+++ b/web-ui/app/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/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..cbe64205
--- /dev/null
+++ b/web-ui/app/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/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..66fe1233
--- /dev/null
+++ b/web-ui/app/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();
+ });
+ }
+
+ }
+);
diff --git a/web-ui/app/js/main.js b/web-ui/app/js/main.js
new file mode 100644
index 00000000..b8836a6b
--- /dev/null
+++ b/web-ui/app/js/main.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/>.
+ */
+
+requirejs.config({
+ baseUrl: '../assets/',
+ paths: {
+ 'mail_list': 'js/mail_list',
+ 'page': 'js/page',
+ 'feedback': 'js/feedback',
+ 'flight': 'bower_components/flight',
+ 'DOMPurify': 'bower_components/DOMPurify/dist/purify.min',
+ 'he': 'bower_components/he/he',
+ 'hbs': 'js/generated/hbs',
+ 'helpers': 'js/helpers',
+ 'lib': 'js/lib',
+ 'views': 'js/views',
+ 'tags': 'js/tags',
+ 'mail_list_actions': 'js/mail_list_actions',
+ 'user_alerts': 'js/user_alerts',
+ 'mail_view': 'js/mail_view',
+ 'dispatchers': 'js/dispatchers',
+ 'services': 'js/services',
+ 'mixins': 'js/mixins',
+ 'search': 'js/search',
+ 'foundation': 'js/foundation',
+ 'features': 'js/features/features',
+ 'i18next': 'bower_components/i18next/i18next',
+ 'i18nextXHRBackend': 'bower_components/i18next-xhr-backend/i18nextXHRBackend',
+ 'i18nextBrowserLanguageDetector': 'bower_components/i18next-browser-languagedetector/i18nextBrowserLanguageDetector',
+ 'quoted-printable': 'bower_components/quoted-printable',
+ 'utf8': 'bower_components/utf8',
+ 'user_settings': 'js/user_settings'
+ }
+});
+
+require([
+ 'flight/lib/compose',
+ 'flight/lib/debug'
+], function(compose, debug){
+ 'use strict';
+ debug.enable(true);
+ debug.events.logAll();
+});
+
+require(
+ [
+ 'flight/lib/compose',
+ 'flight/lib/registry',
+ 'flight/lib/advice',
+ 'flight/lib/logger',
+ 'flight/lib/debug',
+ 'page/events',
+ 'page/default',
+ 'js/monkey_patching/all'
+ ],
+
+ function(compose, registry, advice, withLogging, debug, events, initializeDefault, _monkeyPatched) {
+ 'use strict';
+
+ window.Pixelated = window.Pixelated || {};
+ window.Pixelated.events = events;
+
+ compose.mixin(registry, [advice.withAdvice, withLogging]);
+
+ debug.enable(true);
+ debug.events.logAll();
+
+ initializeDefault('');
+ }
+);
diff --git a/web-ui/app/js/mixins/with_auto_refresh.js b/web-ui/app/js/mixins/with_auto_refresh.js
new file mode 100644
index 00000000..c75fda45
--- /dev/null
+++ b/web-ui/app/js/mixins/with_auto_refresh.js
@@ -0,0 +1,47 @@
+/*
+ * 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(['features'],
+ function (features) {
+ 'use strict';
+
+ function withAutoRefresh(refreshMethod) {
+ return function () {
+ this.defaultAttrs({
+ refreshInterval: 15000
+ });
+
+ this.setupRefresher = function () {
+ clearTimeout(this.attr.refreshTimer);
+ this.attr.refreshTimer = setTimeout(function () {
+ this[refreshMethod]();
+ this.setupRefresher();
+ }.bind(this), this.attr.refreshInterval);
+ };
+
+ this.after('initialize', function () {
+ if (features.isAutoRefreshEnabled()) {
+ this.setupRefresher();
+ }
+ });
+ };
+ }
+
+ return withAutoRefresh;
+ }
+);
+
diff --git a/web-ui/app/js/mixins/with_compose_inline.js b/web-ui/app/js/mixins/with_compose_inline.js
new file mode 100644
index 00000000..b8266f28
--- /dev/null
+++ b/web-ui/app/js/mixins/with_compose_inline.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(
+ [
+ 'page/events',
+ 'views/templates',
+ 'mail_view/data/mail_builder',
+ 'mixins/with_mail_edit_base'
+ ],
+ function(events, templates, mailBuilder, withMailEditBase) {
+ 'use strict';
+
+ function withComposeInline() {
+ this.defaultAttrs({
+ subjectDisplay: '#reply-subject',
+ subjectInput: '#subject-container input',
+ forwardBox: '#forward-box',
+ recipientsDisplay: '#all-recipients'
+ });
+
+ this.openMail = function(ev, data) {
+ this.trigger(document, events.ui.mail.open, {ident: this.attr.mail.ident});
+ };
+
+ this.trashReply = function() {
+ this.trigger(document, events.ui.composeBox.trashReply);
+ this.teardown();
+ };
+
+ 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.renderInlineCompose = function(className, viewData) {
+ this.show();
+ this.render(templates.compose.inlineBox, viewData);
+
+ this.$node.addClass(className);
+ this.select('bodyBox').focus();
+
+ this.enableAutoSave();
+ };
+
+ this.updateIdent = function(ev, data) {
+ this.attr.mail.ident = data.ident;
+ };
+
+ this.discardDraft = function() {
+ this.trashReply();
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.mail.sent, this.openMail);
+ this.on(document, events.mail.deleted, this.trashReply);
+ this.on(document, events.mail.draftSaved, this.updateIdent);
+ });
+
+ withMailEditBase.call(this);
+ }
+
+ return withComposeInline;
+ });
diff --git a/web-ui/app/js/mixins/with_enable_disable_on_event.js b/web-ui/app/js/mixins/with_enable_disable_on_event.js
new file mode 100644
index 00000000..5b28a67b
--- /dev/null
+++ b/web-ui/app/js/mixins/with_enable_disable_on_event.js
@@ -0,0 +1,48 @@
+/*
+ * 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([],
+ function () {
+ 'use strict';
+
+ function withEnableDisableOnEvent(ev) {
+ return function () {
+ this.disableElement = function () {
+ this.$node.attr('disabled', 'disabled');
+ };
+
+ this.enableElement = function () {
+ this.$node.removeAttr('disabled');
+ };
+
+ this.toggleEnabled = function (ev, enable) {
+ if (enable) {
+ this.enableElement();
+ } else {
+ this.disableElement();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, ev, this.toggleEnabled);
+ });
+ };
+ }
+
+ return withEnableDisableOnEvent;
+ }
+);
diff --git a/web-ui/app/js/mixins/with_feature_toggle.js b/web-ui/app/js/mixins/with_feature_toggle.js
new file mode 100644
index 00000000..195b08bc
--- /dev/null
+++ b/web-ui/app/js/mixins/with_feature_toggle.js
@@ -0,0 +1,40 @@
+/*
+ * 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(['features'],
+ function(features) {
+ 'use strict';
+
+ function withFeatureToggle(componentName, behaviorForFeatureOff) {
+ return function() {
+
+ this.around('initialize', _.bind(function(basicInitialize, node, attrs) {
+ if(features.isEnabled(componentName)) {
+ return basicInitialize(node, attrs);
+ }
+ else if (behaviorForFeatureOff){
+ behaviorForFeatureOff.call(this);
+
+ return this;
+ }
+ }, this));
+ };
+ }
+
+ return withFeatureToggle;
+
+});
diff --git a/web-ui/app/js/mixins/with_hide_and_show.js b/web-ui/app/js/mixins/with_hide_and_show.js
new file mode 100644
index 00000000..c8902f61
--- /dev/null
+++ b/web-ui/app/js/mixins/with_hide_and_show.js
@@ -0,0 +1,31 @@
+/*
+ * 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(function(require) {
+ 'use strict';
+
+ function withHideAndShow() {
+ this.hide = function () {
+ this.$node.hide();
+ };
+ this.show = function () {
+ this.$node.show();
+ };
+ }
+
+ return withHideAndShow;
+
+});
diff --git a/web-ui/app/js/mixins/with_mail_edit_base.js b/web-ui/app/js/mixins/with_mail_edit_base.js
new file mode 100644
index 00000000..a088080e
--- /dev/null
+++ b/web-ui/app/js/mixins/with_mail_edit_base.js
@@ -0,0 +1,263 @@
+/*
+ * 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/compose',
+ 'helpers/view_helper',
+ 'mail_view/ui/recipients/recipients',
+ 'mail_view/ui/draft_save_status',
+ 'page/events',
+ 'views/i18n',
+ 'mail_view/ui/send_button',
+ 'mail_view/ui/attachment_icon',
+ 'mail_view/ui/attachment_list',
+ 'flight/lib/utils'
+ ],
+ function (compose, viewHelper, Recipients, DraftSaveStatus, events, i18n, SendButton, AttachmentIcon, attachmentList, utils) {
+ 'use strict';
+
+ function withMailEditBase() {
+
+ this.defaultAttrs({
+ bodyBox: '#text-box',
+ sendButton: '#send-button',
+ attachmentButton: '#attachment-button',
+ attachmentList: '#attachment-list',
+ cancelButton: '#cancel-button',
+ trashButton: '#trash-button',
+ toArea: '#recipients-to-area',
+ toBox: '#recipients-to-box',
+ ccArea: '#recipients-cc-area',
+ bccArea: '#recipients-bcc-area',
+ ccsTrigger: '#ccs-trigger',
+ bccsTrigger: '#bccs-trigger',
+ toTrigger: '#to-trigger',
+ subjectBox: '#subject',
+ tipMsg: '.tip-msg',
+ draftSaveStatus: '#draft-save-status',
+ recipientsFields: '#recipients-fields',
+ currentTag: '',
+ recipientValues: {to: [], cc: [], bcc: []},
+ saveDraftInterval: 3000
+ });
+
+ this.attachRecipients = function (context) {
+ Recipients.attachTo(this.select('toArea'), {name: 'to', addresses: context.recipients.to});
+ Recipients.attachTo(this.select('ccArea'), {name: 'cc', addresses: context.recipients.cc || []});
+ Recipients.attachTo(this.select('bccArea'), {name: 'bcc', addresses: context.recipients.bcc || []});
+ };
+
+ function thereAreRecipientsToDisplay() {
+
+ var allRecipients = _.chain(this.attr.recipientValues).
+ values().
+ flatten().
+ remove(undefined).
+ value();
+
+ return !_.isEmpty(allRecipients);
+ }
+
+ this.warnSendButtonOfRecipients = function () {
+ if (thereAreRecipientsToDisplay.call(this)) {
+ _.forOwn(this.attr.recipientValues, function (recipients, recipientsType) {
+ if (!_.isUndefined(recipients) && !_.isEmpty(recipients)) {
+ var recipientsUpdatedData = {
+ newRecipients: recipients,
+ recipientsName: recipientsType
+ };
+ this.trigger(document, events.ui.recipients.updated, recipientsUpdatedData);
+ }
+ }.bind(this));
+ }
+ };
+
+ this.render = function (template, context) {
+ this.$node.html(template(context));
+
+ if (!context || _.isEmpty(context)) {
+ context.recipients = {to: [], cc: [], bcc: []};
+ }
+ this.attr.recipientValues = context.recipients;
+ this.attr.attachments = context.attachments || [];
+ this.attachRecipients(context);
+
+ this.on(this.select('trashButton'), 'click', this.discardMail);
+ SendButton.attachTo(this.select('sendButton'));
+ AttachmentIcon.attachTo(this.select('attachmentButton'));
+
+ this.warnSendButtonOfRecipients();
+ };
+
+ this.enableAutoSave = function () {
+ this.select('bodyBox').on('input', this.monitorInput.bind(this));
+ this.select('subjectBox').on('input', this.monitorInput.bind(this));
+ this.on(document, events.mail.appendAttachment, this.monitorInput.bind(this));
+ this.on(document, events.mail.removeAttachment, this.monitorInput.bind(this));
+ DraftSaveStatus.attachTo(this.select('draftSaveStatus'));
+ };
+
+ this.monitorInput = function () {
+ this.trigger(events.ui.mail.changedSinceLastSave);
+ this.cancelPostponedSaveDraft();
+ var mail = this.buildMail();
+ this.postponeSaveDraft(mail);
+ };
+
+ this.discardMail = function () {
+ this.cancelPostponedSaveDraft();
+ if (this.attr.ident) {
+ var mail = this.buildMail();
+ this.trigger(document, events.ui.mail.delete, {mail: mail});
+ } else {
+ this.trigger(document, events.ui.mail.discard);
+ }
+ };
+
+ this.trim_recipient = function (recipients) {
+ return recipients.map(function (recipient) {
+ return recipient.trim();
+ });
+ };
+
+ this.sendMail = function () {
+ this.cancelPostponedSaveDraft();
+ var mail = this.buildMail('sent');
+
+ if (allRecipientsAreEmails(mail)) {
+ mail.header.to = this.trim_recipient(mail.header.to);
+ mail.header.cc = this.trim_recipient(mail.header.cc);
+ mail.header.bcc = this.trim_recipient(mail.header.bcc);
+ this.trigger(events.mail.send, mail);
+ } else {
+ this.trigger(
+ events.ui.userAlerts.displayMessage,
+ {message: i18n.t('recipients-not-valid')}
+ );
+ this.trigger(events.mail.send_failed);
+ }
+ };
+
+ this.buildAndSaveDraft = function () {
+ var mail = this.buildMail();
+ this.saveDraft(mail);
+ };
+
+ this.recipientsUpdated = function (ev, data) {
+ this.attr.recipientValues[data.recipientsName] = data.newRecipients;
+ this.trigger(document, events.ui.mail.recipientsUpdated);
+ if (data.skipSaveDraft) {
+ return;
+ }
+
+ var mail = this.buildMail();
+ this.postponeSaveDraft(mail);
+ };
+
+ this.saveDraft = function (mail) {
+ this.cancelPostponedSaveDraft();
+ this.trigger(document, events.mail.saveDraft, mail);
+ };
+
+ this.cancelPostponedSaveDraft = function () {
+ clearTimeout(this.attr.timeout);
+ };
+
+ this.postponeSaveDraft = function (mail) {
+ this.cancelPostponedSaveDraft();
+
+ this.attr.timeout = window.setTimeout(_.bind(function () {
+ this.saveDraft(mail);
+ }, this), this.attr.saveDraftInterval);
+ };
+
+ this.draftSaved = function (event, data) {
+ this.attr.ident = data.ident;
+ };
+
+ this.validateAnyRecipient = function () {
+ return !_.isEmpty(_.flatten(_.values(this.attr.recipientValues)));
+ };
+
+ function allRecipientsAreEmails(mail) {
+ var allRecipients = mail.header.to.concat(mail.header.cc).concat(mail.header.bcc);
+ return _.isEmpty(allRecipients) ? false : _.all(allRecipients, emailFormatChecker);
+ }
+
+ function emailFormatChecker(email) {
+ var emailFormat = /[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailFormat.test(email);
+ }
+
+ this.saveTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ };
+
+ this.mailSent = function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Your message was sent!'});
+ };
+
+ this.enableFloatlabel = function (element) {
+ var showClass = 'showfloatlabel';
+ $(element).bind('keyup', function () {
+ var label = $(this).prev('label');
+ if (this.value !== '') {
+ label.addClass(showClass);
+ $(this).addClass(showClass);
+ } else {
+ label.removeClass(showClass);
+ $(this).removeClass(showClass);
+ }
+ });
+ };
+
+ this.toggleRecipientsArrows = function () {
+ $('#cc-bcc-collapse').toggleClass('fa-angle-down');
+ $('#cc-bcc-collapse').toggleClass('fa-angle-up');
+ };
+
+ this.before('initialize', function () {
+ if (!this.discardDraft) {
+ this.discardDraft = function () {
+ };
+ }
+ });
+
+ this.bindCollapse = function () {
+ this.on($('#cc-bcc-collapse'), 'click', this.toggleRecipientsArrows);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ this.on(document, events.ui.recipients.updated, this.recipientsUpdated);
+ this.on(document, events.mail.draftSaved, this.draftSaved);
+ this.on(document, events.mail.sent, this.mailSent);
+
+ this.on(document, events.ui.mail.send, this.sendMail);
+
+ this.on(document, events.ui.mail.discard, this.discardDraft);
+ this.on(document, events.ui.tag.selected, this.saveTag);
+ this.on(document, events.ui.tag.select, this.saveTag);
+ this.bindCollapse();
+ });
+
+ compose.mixin(this, [attachmentList]);
+ }
+
+ return withMailEditBase;
+ });
diff --git a/web-ui/app/js/mixins/with_mail_sandbox.js b/web-ui/app/js/mixins/with_mail_sandbox.js
new file mode 100644
index 00000000..1a51840d
--- /dev/null
+++ b/web-ui/app/js/mixins/with_mail_sandbox.js
@@ -0,0 +1,80 @@
+/*
+ * 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/view_helper', 'page/events'],
+ function(viewHelpers, events) {
+ 'use strict';
+
+ function withMailSandbox() {
+ this.showMailOnSandbox = function(mail) {
+ var that = this;
+ var $iframe = $("#read-sandbox");
+ var iframe = $iframe[0];
+ var content = viewHelpers.formatMailBody(mail);
+
+ window.addEventListener('message', function(e) {
+ if (e.origin === 'null' && e.source === iframe.contentWindow) {
+ that.trigger(document, events.ui.replyBox.showReplyContainer);
+ that.trigger(document, events.search.highlightResults, {where: '.mail-read-view__header'});
+ }
+ });
+
+ iframe.onload = function() {
+ if ($iframe.iFrameResize) {
+ // use iframe-resizer to dynamically adapt iframe size to its content
+ var config = {
+ resizedCallback: scaleToFit,
+ checkOrigin: false
+ };
+ $iframe.iFrameResize(config);
+ }
+
+ iframe.contentWindow.postMessage({
+ html: content
+ }, '*');
+
+ // transform scale iframe to fit container width
+ // necessary if iframe is wider than container
+ function scaleToFit() {
+ var parentWidth = $iframe.parent().width();
+ var w = $iframe.width();
+ var scale = 'none';
+
+ // only scale html mails
+ if (mail && mail.htmlBody && (w > parentWidth)) {
+ scale = parentWidth / w;
+ scale = 'scale(' + scale + ',' + scale + ')';
+ }
+
+ $iframe.css({
+ '-webkit-transform-origin': '0 0',
+ '-moz-transform-origin': '0 0',
+ '-ms-transform-origin': '0 0',
+ 'transform-origin': '0 0',
+ '-webkit-transform': scale,
+ '-moz-transform': scale,
+ '-ms-transform': scale,
+ 'transform': scale
+ });
+ }
+ };
+ };
+ }
+
+ return withMailSandbox;
+ }
+);
diff --git a/web-ui/app/js/mixins/with_mail_tagging.js b/web-ui/app/js/mixins/with_mail_tagging.js
new file mode 100644
index 00000000..1fc1c3bd
--- /dev/null
+++ b/web-ui/app/js/mixins/with_mail_tagging.js
@@ -0,0 +1,69 @@
+/*
+ * 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(
+ ['page/events', 'features'],
+ function (events, features) {
+ 'use strict';
+ function withMailTagging () {
+ this.updateTags = function(mail, tags) {
+ this.trigger(document, events.mail.tags.update, {ident: mail.ident, tags: tags});
+ };
+
+ this.attachTagCompletion = function(mail) {
+ this.tagFilter = function (parsedResult) {
+ var filtered = _.filter(parsedResult, function (tag) {return ! _.contains(mail.tags, tag.name); });
+ return _.map(filtered, function(tag) { return {value: Handlebars.Utils.escapeExpression(tag.name)}; });
+ };
+
+ this.tagCompleter = new Bloodhound({
+ datumTokenizer: function(d) { return [d.value]; },
+ queryTokenizer: function(q) { return [q.trim()]; },
+ remote: {
+ url: '/tags?skipDefaultTags=true&q=%QUERY',
+ filter: this.tagFilter
+ }
+ });
+
+ this.tagCompleter.initialize();
+
+ this.select('newTagInput').typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 1
+ }, {
+ source: this.tagCompleter.ttAdapter()
+ });
+ };
+
+ this.createNewTag = function () {
+ var tagsCopy = this.attr.mail.tags.slice();
+ tagsCopy.push(this.select('newTagInput').val());
+ this.tagCompleter.clear();
+ this.tagCompleter.clearPrefetchCache();
+ this.tagCompleter.clearRemoteCache();
+ this.updateTags(this.attr.mail, _.uniq(tagsCopy));
+ };
+
+ this.after('displayMail', function () {
+ this.on(this.select('newTagInput'), 'typeahead:selected typeahead:autocompleted', this.createNewTag);
+ });
+ }
+
+ return withMailTagging;
+ }
+);
diff --git a/web-ui/app/js/monkey_patching/all.js b/web-ui/app/js/monkey_patching/all.js
new file mode 100644
index 00000000..2c29c9a1
--- /dev/null
+++ b/web-ui/app/js/monkey_patching/all.js
@@ -0,0 +1,17 @@
+/*
+ * 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/>.
+ */
+require(['js/monkey_patching/array', 'js/monkey_patching/post_message'], function () {});
diff --git a/web-ui/app/js/monkey_patching/array.js b/web-ui/app/js/monkey_patching/array.js
new file mode 100644
index 00000000..d0ccc4b8
--- /dev/null
+++ b/web-ui/app/js/monkey_patching/array.js
@@ -0,0 +1,27 @@
+/*
+ * 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/>.
+ */
+(function () {
+ 'use strict';
+
+ // Array Remove - By John Resig (MIT Licensed)
+ Array.prototype.remove = function (from, to) {
+ var rest = this.slice((to || from) + 1 || this.length);
+ this.length = from < 0 ? this.length + from : from;
+ return this.push.apply(this, rest);
+ };
+
+}());
diff --git a/web-ui/app/js/monkey_patching/post_message.js b/web-ui/app/js/monkey_patching/post_message.js
new file mode 100644
index 00000000..363ce581
--- /dev/null
+++ b/web-ui/app/js/monkey_patching/post_message.js
@@ -0,0 +1,32 @@
+/*
+ * 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/>.
+ */
+/*
+ * origin window.postMessage fails with non serializable objects, so we fallback to console.log to do the job
+ */
+(function () {
+ 'use strict';
+
+ var originalPostMessage = window.postMessage;
+ window.postMessage = function(a, b) {
+ try {
+ originalPostMessage(a, b);
+ } catch (e) {
+ console.log(a, b);
+ }
+ };
+
+}());
diff --git a/web-ui/app/js/page/default.js b/web-ui/app/js/page/default.js
new file mode 100644
index 00000000..ecaedfd8
--- /dev/null
+++ b/web-ui/app/js/page/default.js
@@ -0,0 +1,146 @@
+/*
+ * 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(
+ [
+ 'mail_view/ui/compose_box',
+ 'mail_list_actions/ui/mail_list_actions',
+ 'user_alerts/ui/user_alerts',
+ 'mail_list/ui/mail_list',
+ 'mail_view/ui/no_message_selected_pane',
+ 'mail_view/ui/no_mails_available_pane',
+ 'mail_view/ui/mail_view',
+ 'mail_view/ui/mail_actions',
+ 'mail_view/ui/reply_section',
+ 'mail_view/data/mail_sender',
+ 'services/mail_service',
+ 'services/delete_service',
+ 'services/recover_service',
+ 'tags/ui/tag_list',
+ 'tags/data/tags',
+ 'page/router',
+ 'dispatchers/right_pane_dispatcher',
+ 'dispatchers/middle_pane_dispatcher',
+ 'dispatchers/left_pane_dispatcher',
+ 'search/search_trigger',
+ 'search/results_highlighter',
+ 'foundation/off_canvas',
+ 'page/pane_contract_expand',
+ 'views/i18n',
+ 'views/recipientListFormatter',
+ 'flight/lib/logger',
+ 'user_settings/data/user_settings',
+ 'user_settings/ui/user_settings_icon',
+ 'page/logout',
+ 'page/logout_shortcut',
+ 'feedback/feedback_trigger',
+ 'mail_view/ui/feedback_box',
+ 'mail_view/data/feedback_sender',
+ 'page/version',
+ 'page/unread_count_title',
+ 'page/pix_logo',
+ 'helpers/browser'
+ ],
+
+ function (
+ composeBox,
+ mailListActions,
+ userAlerts,
+ mailList,
+ noMessageSelectedPane,
+ noMailsAvailablePane,
+ mailView,
+ mailViewActions,
+ replyButton,
+ mailSender,
+ mailService,
+ deleteService,
+ recoverService,
+ tagList,
+ tags,
+ router,
+ rightPaneDispatcher,
+ middlePaneDispatcher,
+ leftPaneDispatcher,
+ searchTrigger,
+ resultsHighlighter,
+ offCanvas,
+ paneContractExpand,
+ viewI18n,
+ recipientListFormatter,
+ withLogging,
+ userSettings,
+ userSettingsIcon,
+ logout,
+ logoutShortcut,
+ feedback,
+ feedbackBox,
+ feedbackSender,
+ version,
+ unreadCountTitle,
+ pixLogo,
+ browser) {
+
+ 'use strict';
+ function initialize(path) {
+ viewI18n.init(path + '/assets/');
+ viewI18n.loaded(function() {
+ paneContractExpand.attachTo(document);
+
+ userAlerts.attachTo('#user-alerts');
+
+ mailList.attachTo('#mail-list');
+ mailListActions.attachTo('#list-actions');
+
+ searchTrigger.attachTo('#search-trigger');
+ resultsHighlighter.attachTo(document);
+
+ mailSender.attachTo(document);
+
+ mailService.attachTo(document);
+ deleteService.attachTo(document);
+ recoverService.attachTo(document);
+
+ tags.attachTo(document);
+ tagList.attachTo('#tag-list');
+
+ router.attachTo(document);
+
+ rightPaneDispatcher.attachTo(document);
+ middlePaneDispatcher.attachTo(document);
+ leftPaneDispatcher.attachTo(document);
+
+ offCanvas.attachTo(document);
+ userSettings.attachTo(document);
+ userSettingsIcon.attachTo('#user-settings-icon');
+ logout.attachTo('#logout');
+ logoutShortcut.attachTo('#logout-shortcut');
+ version.attachTo('.version');
+
+ feedback.attachTo('#feedback');
+ feedbackSender.attachTo(document);
+
+ unreadCountTitle.attachTo(document);
+
+ pixLogo.attachTo(document);
+
+ $.ajaxSetup({headers: {'X-XSRF-TOKEN': browser.getCookie('XSRF-TOKEN')}});
+ });
+ }
+
+ return initialize;
+ }
+);
diff --git a/web-ui/app/js/page/events.js b/web-ui/app/js/page/events.js
new file mode 100644
index 00000000..68a6aad1
--- /dev/null
+++ b/web-ui/app/js/page/events.js
@@ -0,0 +1,222 @@
+/*
+ * 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(function () {
+ 'use strict';
+
+ var events = {
+ router: {
+ pushState: 'router:pushState'
+ },
+ ui: {
+ sendbutton: {
+ enable: 'ui:sendbutton:enable'
+ },
+ middlePane: {
+ expand: 'ui:middlePane:expand',
+ contract: 'ui:middlePane:contract'
+ },
+ userAlerts: {
+ displayMessage: 'ui:userAlerts:displayMessage'
+ },
+ tag: {
+ selected: 'ui:tagSelected',
+ select: 'ui:tagSelect',
+ },
+ tags: {
+ loaded: 'ui:tagsLoaded'
+ },
+ tagList: {
+ load: 'ui:tagList:load'
+ },
+ mails: {
+ refresh: 'ui:mails:refresh',
+ fetchByTag: 'ui:mails:fetchByTag',
+ cleanSelected: 'ui:mails:cleanSelected',
+ checkAll: 'ui:mails:checkAll',
+ uncheckAll: 'ui:mails:uncheckAll',
+ hasMailsChecked: 'ui:mails:hasMailsChecked'
+ },
+ mail: {
+ open: 'ui:mail:open',
+ updateSelected: 'ui:mail:updateSelected',
+ delete: 'ui:mail:delete',
+ deleteMany: 'ui:mail:deleteMany',
+ recoverMany: 'ui:mail:recoverMany',
+ archiveMany: 'ui:mail:archiveMany',
+ wantChecked: 'ui:mail:wantChecked',
+ hereChecked: 'ui:mail:hereChecked',
+ checked: 'ui:mail:checked',
+ discard: 'ui:mail:discard',
+ unchecked: 'ui:mail:unchecked',
+ changedSinceLastSave: 'ui:mail:changedSinceLastSave',
+ send: 'ui:mail:send',
+ recipientsUpdated: 'ui:mail:recipientsUpdated'
+ },
+ page: {
+ previous: 'ui:page:previous',
+ next: 'ui:page:next',
+ changed: 'ui:page:changed',
+ spinLogo: 'ui:page:spinLogo',
+ stopSpinningLogo: 'ui:page:stopSpinningLogo'
+ },
+ composeBox: {
+ newMessage: 'ui:composeBox:newMessage',
+ newReply: 'ui:composeBox:newReply',
+ trashReply: 'ui:composeBox:trashReply',
+ requestCancelReply: 'ui:composeBox:requestCancelReply'
+ },
+ replyBox: {
+ showReply: 'ui:replyBox:showReply',
+ showReplyAll: 'ui:replyBox:showReplyAll',
+ showReplyContainer: 'ui:replyBox:showReplyContainer',
+ },
+ recipients: {
+ entered: 'ui:recipients:entered',
+ enteredInvalid: 'ui:recipients:enteredInvalid',
+ updated: 'ui:recipients:updated',
+ editRecipient: 'ui:recipients:editRecipient',
+ deleteRecipient: 'ui:recipients:deleteRecipient',
+ deleteLast: 'ui:recipients:deleteLast',
+ selectLast: 'ui:recipients:selectLast',
+ unselectAll: 'ui:recipients:unselectAll',
+ addressesExist: 'ui:recipients:addressesExist',
+ inputFieldHasCharacters: 'ui:recipients:inputFieldHasCharacters',
+ inputFieldIsEmpty: 'ui:recipients:inputFieldIsEmpty',
+ doCompleteInput: 'ui:recipients:doCompleteInput',
+ doCompleteRecipients: 'ui:recipients:doCompleteRecipients',
+ clickToEdit: 'ui:recipients:clickToEdit'
+ },
+ userSettingsBox: {
+ toggle: 'ui:userSettingsBox:toggle'
+ }
+ },
+ search: {
+ perform: 'search:perform',
+ results: 'search:results',
+ empty: 'search:empty',
+ highlightResults: 'search:highlightResults',
+ resetHighlight: 'search:resetHighlight'
+ },
+ feedback: {
+ submit: 'feedback:submit',
+ submitted: 'feedback:submitted'
+ },
+ userSettings: {
+ here: 'userSettings:here',
+ getInfo: 'userSettings:getInfo',
+ destroyPopup: 'userSettings:destroyPopup'
+ },
+ mail: {
+ here: 'mail:here',
+ want: 'mail:want',
+ display: 'mail:display',
+ highlightMailContent: 'mail:highlightMailContent',
+ send: 'mail:send',
+ send_failed: 'mail:send_failed',
+ sent: 'mail:sent',
+ read: 'mail:read',
+ unread: 'mail:unread',
+ delete: 'mail:delete',
+ deleteMany: 'mail:deleteMany',
+ archiveMany: 'mail:archiveMany',
+ recoverMany: 'mail:recoverMany',
+ deleted: 'mail:deleted',
+ saveDraft: 'draft:save',
+ draftSaved: 'draft:saved',
+ draftReply: {
+ want: 'mail:draftReply:want',
+ here: 'mail:draftReply:here',
+ notFound: 'mail:draftReply:notFound'
+ },
+ notFound: 'mail:notFound',
+ save: 'mail:saved',
+ tags: {
+ update: 'mail:tags:update',
+ updated: 'mail:tags:updated'
+ },
+ uploadedAttachment: 'mail:uploaded:attachment',
+ uploadingAttachment: 'mail:uploading:attachment',
+ startUploadAttachment: 'mail:start:upload:attachment',
+ failedUploadAttachment: 'mail:failed:upload:attachment',
+ appendAttachment: 'mail:append:attachment',
+ resetAttachments: 'mail:reset:attachments',
+ removeAttachment: 'mail:remove:attachment'
+ },
+ mails: {
+ available: 'mails:available',
+ availableForRefresh: 'mails:available:refresh',
+ teardown: 'mails:teardown'
+ },
+ tags: {
+ want: 'tags:want',
+ received: 'tags:received',
+ teardown: 'tags:teardown',
+ shortcuts: {
+ teardown: 'tags:shortcuts:teardown'
+ }
+ },
+ route: {
+ toUrl: 'route:toUrl'
+ },
+
+ components: {
+ composeBox: {
+ open: 'components:composeBox:open',
+ close: 'components:composeBox:close'
+ },
+ mailPane: {
+ open: 'components:mailPane:open',
+ close: 'components:mailPane:close'
+ },
+ mailView: {
+ show: 'components:mailView:show',
+ close: 'components:mailView:close'
+ },
+ replySection: {
+ initialize: 'components:replySection:initialize',
+ close: 'components:replySection:close'
+ },
+ noMessageSelectedPane: {
+ open: 'components:noMessageSelectedPane:open',
+ close: 'components:noMessageSelectedPane:close'
+ }
+ },
+
+ dispatchers: {
+ rightPane: {
+ openComposeBox: 'dispatchers:rightPane:openComposeBox',
+ openFeedbackBox: 'dispatchers:rightPane:openFeedbackBox',
+ openNoMessageSelected: 'dispatchers:rightPane:openNoMessageSelected',
+ openNoMessageSelectedWithoutPushState: 'dispatchers:rightPane:openNoMessageSelectedWithoutPushState',
+ refreshMailList: 'dispatchers:rightPane:refreshMailList',
+ openDraft: 'dispatchers:rightPane:openDraft',
+ selectTag: 'dispatchers:rightPane:selectTag',
+ clear: 'dispatchers:rightPane:clear'
+ },
+ middlePane: {
+ refreshMailList: 'dispatchers:middlePane:refreshMailList',
+ cleanSelected: 'dispatchers:middlePane:unselect',
+ resetScroll: 'dispatchers:middlePane:resetScroll'
+ },
+ tags: {
+ refreshTagList: 'dispatchers:tag:refresh'
+ }
+ }
+ };
+
+ return events;
+});
diff --git a/web-ui/app/js/page/logout.js b/web-ui/app/js/page/logout.js
new file mode 100644
index 00000000..81b57db2
--- /dev/null
+++ b/web-ui/app/js/page/logout.js
@@ -0,0 +1,43 @@
+/*
+ * 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', 'features', 'views/templates', 'helpers/browser'],
+ function (defineComponent, features, templates, browser) {
+ 'use strict';
+
+ return defineComponent(function () {
+
+ this.defaultAttrs({form: '#logout-form'});
+
+ this.render = function () {
+ var logoutHTML = templates.page.logout({ logout_url: features.getLogoutUrl(),
+ csrf_token: browser.getCookie('XSRF-TOKEN')});
+ this.$node.html(logoutHTML);
+ };
+
+ this.logout = function(){
+ this.select('form').submit();
+ };
+
+ this.after('initialize', function () {
+ if (features.isLogoutEnabled()) {
+ this.render();
+ this.on(this.$node, 'click', this.logout);
+ }
+ });
+
+ });
+});
diff --git a/web-ui/app/js/page/logout_shortcut.js b/web-ui/app/js/page/logout_shortcut.js
new file mode 100644
index 00000000..10a69c7d
--- /dev/null
+++ b/web-ui/app/js/page/logout_shortcut.js
@@ -0,0 +1,33 @@
+/*
+ * 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', 'features', 'views/templates'], function (defineComponent, features, templates) {
+ 'use strict';
+
+ return defineComponent(function () {
+
+ this.render = function () {
+ if (features.isLogoutEnabled()) {
+ var logoutShortcutHTML = templates.page.logoutShortcut();
+ this.$node.html(logoutShortcutHTML);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ });
+ });
+});
diff --git a/web-ui/app/js/page/pane_contract_expand.js b/web-ui/app/js/page/pane_contract_expand.js
new file mode 100644
index 00000000..9bb435c4
--- /dev/null
+++ b/web-ui/app/js/page/pane_contract_expand.js
@@ -0,0 +1,51 @@
+/*
+ * 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'], function (describeComponent, events) {
+ 'use strict';
+
+ return describeComponent(paneContractExpand);
+
+ function paneContractExpand() {
+ this.defaultAttrs({
+ RIGHT_PANE_EXPAND_CLASSES: 'small-7 medium-7 large-7 columns',
+ RIGHT_PANE_CONTRACT_CLASSES: 'small-7 medium-4 large-4 columns',
+ MIDDLE_PANE_EXPAND_CLASSES: 'small-5 medium-8 large-8 columns no-padding',
+ MIDDLE_PANE_CONTRACT_CLASSES: 'small-5 medium-5 large-5 columns no-padding'
+ });
+
+ this.expandMiddlePaneContractRightPane = function () {
+ $('#middle-pane-container').attr('class', this.attr.MIDDLE_PANE_EXPAND_CLASSES);
+ $('#right-pane').attr('class', this.attr.RIGHT_PANE_CONTRACT_CLASSES);
+ };
+
+ this.contractMiddlePaneExpandRightPane = function () {
+ $('#middle-pane-container').attr('class', this.attr.MIDDLE_PANE_CONTRACT_CLASSES);
+ $('#right-pane').attr('class', this.attr.RIGHT_PANE_EXPAND_CLASSES);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mail.open, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openComposeBox, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openDraft, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openFeedbackBox, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openNoMessageSelected, this.expandMiddlePaneContractRightPane);
+ this.expandMiddlePaneContractRightPane();
+ });
+
+ }
+});
diff --git a/web-ui/app/js/page/pix_logo.js b/web-ui/app/js/page/pix_logo.js
new file mode 100644
index 00000000..ad17f3be
--- /dev/null
+++ b/web-ui/app/js/page/pix_logo.js
@@ -0,0 +1,62 @@
+/*
+ * 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'
+ ],
+
+ function(defineComponent, events) {
+ 'use strict';
+
+ return defineComponent(pixLogo);
+
+ function pixLogo() {
+ this.turnAnimationOn = function () {
+ $('.logo-part-animation-off').attr('class', 'logo-part-animation-on');
+ };
+
+ this.turnAnimationOff = function () {
+ setTimeout(function(){
+ $('.logo-part-animation-on').attr('class', 'logo-part-animation-off');
+ }, 600);
+ };
+
+ this.triggerSpinLogo = function (ev, data) {
+ this.trigger(document, events.ui.page.spinLogo);
+ };
+
+ this.triggerStopSpinningLogo = function(ev, data) {
+ this.trigger(document, events.ui.page.stopSpinningLogo);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.page.spinLogo, this.turnAnimationOn);
+ this.on(document, events.ui.page.stopSpinningLogo, this.turnAnimationOff);
+
+ this.on(document, events.ui.tag.select, this.triggerSpinLogo);
+ this.on(document, events.mails.available, this.triggerStopSpinningLogo);
+ this.on(document, events.mail.saveDraft, this.triggerSpinLogo);
+ this.on(document, events.mail.draftSaved, this.triggerStopSpinningLogo);
+ this.on(document, events.ui.mail.open, this.triggerSpinLogo);
+ this.on(document, events.dispatchers.rightPane.openDraft, this.triggerSpinLogo);
+ this.on(document, events.search.perform, this.triggerSpinLogo);
+ this.on(document, events.mail.want, this.triggerStopSpinningLogo);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/page/router.js b/web-ui/app/js/page/router.js
new file mode 100644
index 00000000..ce0d7d04
--- /dev/null
+++ b/web-ui/app/js/page/router.js
@@ -0,0 +1,71 @@
+/*
+ * 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', 'page/router/url_params'], function (defineComponent, events, urlParams) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ history: window.history
+ });
+
+ function createHash(data) {
+ var hash = '/#/' + data.tag;
+ if (!_.isUndefined(data.mailIdent)) {
+ hash += '/mail/' + data.mailIdent;
+ }
+ return hash;
+ }
+
+ function createState(data, previousState) {
+ return {
+ tag: data.tag || (previousState && previousState.tag) || urlParams.defaultTag(),
+ mailIdent: data.mailIdent,
+ query: data.query,
+ isDisplayNoMessageSelected: !!data.isDisplayNoMessageSelected
+ };
+ }
+
+ this.pushState = function (ev, data) {
+ if (!data.fromPopState) {
+ var nextState = createState(data, this.attr.history.state);
+ this.attr.history.pushState(nextState, '', createHash(nextState));
+ }
+ };
+
+ this.popState = function (ev) {
+ var state = ev.state || {};
+
+ this.trigger(document, events.ui.tag.select, {
+ tag: state.tag || urlParams.getTag(),
+ mailIdent: state.mailIdent,
+ fromPopState: true
+ });
+
+ if (ev.state.isDisplayNoMessageSelected) {
+ this.trigger(document, events.dispatchers.rightPane.openNoMessageSelectedWithoutPushState);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.router.pushState, this.pushState);
+ this.on(document, events.ui.tag.select, this.pushState);
+ this.on(document, events.search.perform, this.pushState);
+ this.on(document, events.search.empty, this.pushState);
+ window.onpopstate = this.popState.bind(this);
+ });
+ });
+});
diff --git a/web-ui/app/js/page/router/url_params.js b/web-ui/app/js/page/router/url_params.js
new file mode 100644
index 00000000..4fa11c6d
--- /dev/null
+++ b/web-ui/app/js/page/router/url_params.js
@@ -0,0 +1,57 @@
+/*
+ * 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([], function () {
+ 'use strict';
+
+ function defaultTag() {
+ return 'inbox';
+ }
+
+ function getDocumentHash() {
+ return document.location.hash.replace(/\/$/, '');
+ }
+
+ function hashTag(hash) {
+ if (hasMailIdent(hash)) {
+ return /\/(.+)\/mail\/[-\w]+$/.exec(getDocumentHash())[1];
+ }
+ return hash.substring(2);
+ }
+
+
+ function getTag() {
+ if (document.location.hash !== '') {
+ return hashTag(getDocumentHash());
+ }
+ return defaultTag();
+ }
+
+ function hasMailIdent() {
+ return getDocumentHash().match(/mail\/[-\w]+$/);
+ }
+
+ function getMailIdent() {
+ return /mail\/([-\w]+)$/.exec(getDocumentHash())[1];
+ }
+
+ return {
+ getTag: getTag,
+ hasMailIdent: hasMailIdent,
+ getMailIdent: getMailIdent,
+ defaultTag: defaultTag
+ };
+});
diff --git a/web-ui/app/js/page/unread_count_title.js b/web-ui/app/js/page/unread_count_title.js
new file mode 100644
index 00000000..89dcd47d
--- /dev/null
+++ b/web-ui/app/js/page/unread_count_title.js
@@ -0,0 +1,53 @@
+/*
+ * 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',
+ ],
+
+ function (defineComponent, events) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.getTitleText = function () {
+ return document.title;
+ };
+
+ this.updateCount = function (ev, data) {
+ var unread = data.mails.filter(function (mail) {
+ return mail.status.indexOf('read') === -1;
+ }).length;
+
+ var tag = this.toTitleCase(data.tag);
+ var counter = unread > 0 ? ' (' + unread + ') - ' : ' - ';
+ document.title = tag + counter + this.rawTitle;
+ };
+
+ this.toTitleCase = function (str) {
+ return str.replace(/\b\w/g, function (txt) { return txt.toUpperCase(); });
+ };
+
+ this.after('initialize', function () {
+ this.rawTitle = document.title;
+ this.on(document, events.mails.available, this.updateCount);
+ });
+
+ });
+});
diff --git a/web-ui/app/js/page/version.js b/web-ui/app/js/page/version.js
new file mode 100644
index 00000000..9fd5e629
--- /dev/null
+++ b/web-ui/app/js/page/version.js
@@ -0,0 +1,41 @@
+/*
+ * 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', 'helpers/view_helper'], function (defineComponent, templates, viewHelper) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ 'sinceDate': '#version-date'
+ });
+
+ this.render = function () {
+ this.$node.html(templates.page.version());
+ this.renderCommitDate();
+ };
+
+ this.renderCommitDate = function(){
+ var since = this.select('sinceDate').attr('data-since'),
+ commitDate = viewHelper.sinceDate(since);
+ this.select('sinceDate').html(commitDate + ' ago');
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ });
+
+ });
+});
diff --git a/web-ui/app/js/sandbox.js b/web-ui/app/js/sandbox.js
new file mode 100644
index 00000000..33b16ea4
--- /dev/null
+++ b/web-ui/app/js/sandbox.js
@@ -0,0 +1,11 @@
+(function () {
+ 'use strict';
+
+ window.onmessage = function (e) {
+ if (e.data.html) {
+ document.body.innerHTML = e.data.html;
+ var mainWindow = e.source;
+ mainWindow.postMessage('data ok', e.origin);
+ }
+ };
+})();
diff --git a/web-ui/app/js/search/results_highlighter.js b/web-ui/app/js/search/results_highlighter.js
new file mode 100644
index 00000000..831be0cd
--- /dev/null
+++ b/web-ui/app/js/search/results_highlighter.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',
+ 'page/events'
+ ], function (defineComponent, events) {
+
+ 'use strict';
+
+ return defineComponent(resultsHighlighter);
+
+ function resultsHighlighter(){
+ this.defaultAttrs({
+ keywords: []
+ });
+
+ this.getKeywordsSearch = function (event, data) {
+ this.attr.keywords = data.query.split(' ').map(function(keyword) {
+ return keyword.toLowerCase();
+ });
+ };
+
+ this.highlightResults = function (event, data) {
+ var domIdent = data.where;
+ if(this.attr.keywords) {
+ _.each(this.attr.keywords, function (keyword) {
+ keyword = escapeRegExp(keyword);
+ $(domIdent).highlightRegex(new RegExp(keyword, 'i'), {
+ tagType: 'em',
+ className: 'search-highlight'
+ });
+ });
+ }
+ };
+
+ this.clearHighlights = function (event, data) {
+ this.attr.keywords = [];
+ _.each($('em.search-highlight'), function(highlighted) {
+ var jqueryHighlighted = $(highlighted);
+ var text = jqueryHighlighted.text();
+ jqueryHighlighted.replaceWith(text);
+ });
+ };
+
+ this.highlightString = function (string) {
+ _.each(this.attr.keywords, function (keyword) {
+ keyword = escapeRegExp(keyword);
+ var regex = new RegExp('(' + keyword + ')', 'ig');
+ string = string.replace(regex, '<em class="search-highlight">$1</em>');
+ });
+ return string;
+ };
+
+ /*
+ * Alter data.mail.textPlainBody to highlight each of this.attr.keywords
+ * and pass it back to the mail_view when done
+ */
+ this.highlightMailContent = function(ev, data){
+ var mail = data.mail;
+ mail.textPlainBody = this.highlightString(mail.textPlainBody);
+ this.trigger(document, events.mail.display, data);
+ };
+
+ /*
+ * Escapes the special charaters used regular expressions that
+ * would cause problems with strings in the RegExp constructor
+ */
+ function escapeRegExp(string){
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ }
+
+ this.after('initialize', function () {
+ this.on(document, events.search.perform, this.getKeywordsSearch);
+ this.on(document, events.ui.tag.select, this.clearHighlights);
+ this.on(document, events.search.resetHighlight, this.clearHighlights);
+
+ this.on(document, events.search.highlightResults, this.highlightResults);
+ this.on(document, events.mail.highlightMailContent, this.highlightMailContent);
+ });
+ }
+});
diff --git a/web-ui/app/js/search/search_trigger.js b/web-ui/app/js/search/search_trigger.js
new file mode 100644
index 00000000..2aff027c
--- /dev/null
+++ b/web-ui/app/js/search/search_trigger.js
@@ -0,0 +1,81 @@
+/*
+ * 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',
+ 'views/i18n'
+ ], function (defineComponent, templates, events, i18n) {
+
+ 'use strict';
+
+ return defineComponent(searchTrigger);
+
+ function searchTrigger() {
+ this.defaultAttrs({
+ input: 'input[type=search]',
+ form: 'form',
+ searchResultsPrefix: 'search-results-for'
+ });
+
+ this.render = function() {
+ this.$node.html(templates.search.trigger());
+ };
+
+ this.search = function(ev, data) {
+ this.trigger(document, events.search.resetHighlight);
+ ev.preventDefault();
+ var input = this.select('input');
+ var value = input.val();
+ input.blur();
+ if(!_.isEmpty(value)){
+ this.trigger(document, events.search.perform, { query: value });
+ } else {
+ this.trigger(document, events.search.empty);
+ }
+ };
+
+ this.clearInput = function() {
+ this.select('input').val('');
+ };
+
+ this.showOnlySearchTerms = function(event){
+ var value = this.select('input').val();
+ var searchTerms = value.slice((i18n.t(this.attr.searchResultsPrefix) + ': ').length);
+ this.select('input').val(searchTerms);
+ };
+
+ this.showSearchTermsAndPlaceHolder = function(event){
+ var value = this.select('input').val();
+ if (value.length > 0){
+ this.select('input').val(i18n.t(this.attr.searchResultsPrefix) + ': ' + value);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on(this.select('form'), 'submit', this.search);
+ this.on(this.select('input'), 'focus', this.showOnlySearchTerms);
+ this.on(this.select('input'), 'blur', this.showSearchTermsAndPlaceHolder);
+ this.on(document, events.ui.tag.selected, this.clearInput);
+ this.on(document, events.ui.tag.select, this.clearInput);
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/services/delete_service.js b/web-ui/app/js/services/delete_service.js
new file mode 100644
index 00000000..0dfc1bdb
--- /dev/null
+++ b/web-ui/app/js/services/delete_service.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(['flight/lib/component', 'page/events', 'views/i18n'], function (defineComponent, events, i18n) {
+ 'use strict';
+
+ return defineComponent(function() {
+
+ this.successDeleteMessageFor = function(mail) {
+ return mail.isInTrash() ?
+ i18n.t('delete-single') :
+ i18n.t('trash-single');
+ };
+
+ this.successDeleteManyMessageFor = function(mail) {
+ return mail.isInTrash() ?
+ i18n.t('delete-bulk') :
+ i18n.t('trash-bulk');
+ };
+
+ this.deleteEmail = function (event, data) {
+ this.trigger(document, events.mail.delete, {
+ mail: data.mail,
+ successMessage: this.successDeleteMessageFor(data.mail)
+ });
+ };
+
+ this.deleteManyEmails = function (event, data) {
+ var emails = _.values(data.checkedMails),
+ firstEmail = emails[_.first(_.keys(emails))];
+
+ this.trigger(document, events.mail.deleteMany, {
+ mails: emails,
+ successMessage: this.successDeleteManyMessageFor(firstEmail)
+ });
+
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mail.delete, this.deleteEmail);
+ this.on(document, events.ui.mail.deleteMany, this.deleteManyEmails);
+ });
+
+ });
+});
diff --git a/web-ui/app/js/services/mail_service.js b/web-ui/app/js/services/mail_service.js
new file mode 100644
index 00000000..5e4bd4f3
--- /dev/null
+++ b/web-ui/app/js/services/mail_service.js
@@ -0,0 +1,335 @@
+/*
+ * 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/i18n',
+ 'services/model/mail',
+ 'helpers/monitored_ajax',
+ 'page/events',
+ 'features',
+ 'mixins/with_auto_refresh',
+ 'page/router/url_params'
+ ], function (defineComponent, i18n, Mail, monitoredAjax, events, features, withAutoRefresh, urlParams) {
+
+ 'use strict';
+
+ return defineComponent(mailService, withAutoRefresh('refreshMails'));
+
+ function mailService() {
+ var that;
+
+ this.defaultAttrs({
+ mailsResource: '/mails',
+ singleMailResource: '/mail',
+ currentTag: '',
+ lastQuery: '',
+ currentPage: 1,
+ numPages: 1,
+ pageSize: 25
+ });
+
+ this.errorMessage = function (msg) {
+ return function () {
+ that.trigger(document, events.ui.userAlerts.displayMessage, { message: msg });
+ };
+ };
+
+ this.updateTags = function (ev, data) {
+ var ident = data.ident;
+
+ var success = function (data) {
+ this.refreshMails();
+ $(document).trigger(events.mail.tags.updated, { ident: ident, tags: data.tags });
+ $(document).trigger(events.dispatchers.tags.refreshTagList, { skipMailListRefresh: true });
+ };
+
+ var failure = function (resp) {
+ var msg = i18n.t('failed-change-tags');
+ if (resp.status === 403) {
+ msg = i18n.t('invalid-tag-name');
+ }
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: msg });
+ };
+
+ monitoredAjax(this, '/mail/' + ident + '/tags', {
+ type: 'POST',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({newtags: data.tags})
+ }).done(success.bind(this)).fail(failure.bind(this));
+
+ };
+
+ this.readMail = function (ev, data) {
+ var mailIdents;
+ if (data.checkedMails) {
+ mailIdents = _.map(data.checkedMails, function (mail) {
+ return mail.ident;
+ });
+ } else {
+ mailIdents = [data.ident];
+ }
+ monitoredAjax(this, '/mails/read', {
+ type: 'POST',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerMailsRead(data.checkedMails));
+ };
+
+ this.unreadMail = function (ev, data) {
+ var mailIdents;
+ if (data.checkedMails) {
+ mailIdents = _.map(data.checkedMails, function (mail) {
+ return mail.ident;
+ });
+ } else {
+ mailIdents = [data.ident];
+ }
+ monitoredAjax(this, '/mails/unread', {
+ type: 'POST',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerMailsRead(data.checkedMails));
+ };
+
+ this.triggerMailsRead = function (mails) {
+ return _.bind(function () {
+ this.refreshMails();
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }, this);
+ };
+
+ this.triggerDeleted = function (dataToDelete) {
+ return _.bind(function () {
+ var mails = dataToDelete.mails || [dataToDelete.mail];
+
+ this.refreshMails();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: dataToDelete.successMessage});
+ this.trigger(document, events.ui.mails.uncheckAll);
+ this.trigger(document, events.mail.deleted, { mails: mails });
+ }, this);
+ };
+
+ this.triggerRecovered = function (dataToRecover) {
+ return _.bind(function () {
+ var mails = dataToRecover.mails || [dataToRecover.mail];
+
+ this.refreshMails();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.t(dataToRecover.successMessage)});
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }, this);
+ };
+
+ this.triggerArchived = function (dataToArchive) {
+ return _.bind(function (response) {
+ this.refreshMails();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.t(response.successMessage)});
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }, this);
+ };
+
+ this.archiveManyMails = function(event, dataToArchive) {
+ var mailIdents = _.map(dataToArchive.checkedMails, function (mail) {
+ return mail.ident;
+ });
+ monitoredAjax(this, '/mails/archive', {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerArchived(dataToArchive))
+ .fail(this.errorMessage(i18n.t('failed-archive')));
+ };
+
+ this.deleteMail = function (ev, data) {
+ monitoredAjax(this, '/mail/' + data.mail.ident,
+ {type: 'DELETE'})
+ .done(this.triggerDeleted(data))
+ .fail(this.errorMessage(i18n.t('failed-delete-single')));
+ };
+
+ this.deleteManyMails = function (ev, data) {
+ var dataToDelete = data;
+ var mailIdents = _.map(data.mails, function (mail) {
+ return mail.ident;
+ });
+
+ monitoredAjax(this, '/mails/delete', {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerDeleted(dataToDelete))
+ .fail(this.errorMessage(i18n.t('failed-delete-bulk')));
+ };
+
+ this.recoverManyMails = function (ev, data) {
+ var dataToRecover = data;
+ var mailIdents = _.map(data.mails, function (mail) {
+ return mail.ident;
+ });
+
+ monitoredAjax(this, '/mails/recover', {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerRecovered(dataToRecover))
+ .fail(this.errorMessage(i18n.t('Could not move emails to inbox')));
+ };
+
+ function compileQuery(data) {
+ var query = 'tag:"' + that.attr.currentTag + '"';
+
+ if (data.tag === 'all') {
+ query = 'in:all';
+ }
+ return query;
+ }
+
+ this.fetchByTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ this.attr.lastQuery = compileQuery(data);
+ this.updateCurrentPageNumber(1);
+
+ this.refreshMails();
+ };
+
+ this.newSearch = function (ev, data) {
+ this.attr.lastQuery = data.query;
+ this.attr.currentTag = 'all';
+ this.refreshMails();
+ };
+
+ this.mailFromJSON = function (mail) {
+ return Mail.create(mail);
+ };
+
+ this.parseMails = function (data) {
+ data.mails = _.map(data.mails, this.mailFromJSON, this);
+
+ return data;
+ };
+
+ function escaped(s) {
+ return encodeURIComponent(s);
+ }
+
+ this.excludeTrashedEmailsForDraftsAndSent = function (query) {
+ if (query === 'tag:"drafts"' || query === 'tag:"sent"') {
+ return query + ' -in:"trash"';
+ }
+ return query;
+ };
+
+ this.refreshMails = function () {
+ var url = this.attr.mailsResource + '?q=' + escaped(this.attr.lastQuery) + '&p=' + this.attr.currentPage + '&w=' + this.attr.pageSize;
+
+ this.attr.lastQuery = this.excludeTrashedEmailsForDraftsAndSent(this.attr.lastQuery);
+
+ monitoredAjax(this, url, { dataType: 'json' })
+ .done(function (data) {
+ this.attr.numPages = Math.ceil(data.stats.total / this.attr.pageSize);
+ this.trigger(document, events.mails.available, _.merge({tag: this.attr.currentTag, forSearch: this.attr.lastQuery }, this.parseMails(data)));
+ }.bind(this))
+ .fail(function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.t('failed-fetch-messages'), class: 'error' });
+ }.bind(this));
+ };
+
+ function createSingleMailUrl(mailsResource, ident) {
+ return mailsResource + '/' + ident;
+ }
+
+ this.fetchSingle = function (event, data) {
+ var fetchUrl = createSingleMailUrl(this.attr.singleMailResource, data.mail);
+
+ monitoredAjax(this, fetchUrl, { dataType: 'json' })
+ .done(function (mail) {
+ if (_.isNull(mail)) {
+ this.trigger(data.caller, events.mail.notFound);
+ return;
+ }
+
+ this.trigger(data.caller, events.mail.here, { mail: this.mailFromJSON(mail) });
+ }.bind(this));
+ };
+
+ this.previousPage = function () {
+ if (this.attr.currentPage > 1) {
+ this.updateCurrentPageNumber(this.attr.currentPage - 1);
+ this.refreshMails();
+ }
+ };
+
+ this.nextPage = function () {
+ if (this.attr.currentPage < (this.attr.numPages)) {
+ this.updateCurrentPageNumber(this.attr.currentPage + 1);
+ this.refreshMails();
+ }
+ };
+
+ this.updateCurrentPageNumber = function (newCurrentPage) {
+ this.attr.currentPage = newCurrentPage;
+ this.trigger(document, events.ui.page.changed, {
+ currentPage: this.attr.currentPage,
+ numPages: this.attr.numPages
+ });
+ };
+
+ this.wantDraftReplyForMail = function (ev, data) {
+ if (!features.isEnabled('draftReply')) {
+ this.trigger(document, events.mail.draftReply.notFound);
+ return;
+ }
+
+ monitoredAjax(this, '/draft_reply_for/' + data.ident, { dataType: 'json' })
+ .done(function (mail) {
+ if (_.isNull(mail)) {
+ this.trigger(document, events.mail.draftReply.notFound);
+ return;
+ }
+ this.trigger(document, events.mail.draftReply.here, { mail: this.mailFromJSON(mail) });
+ }.bind(this));
+ };
+
+ this.after('initialize', function () {
+ that = this;
+
+ if (features.isEnabled('tags')) {
+ this.on(events.mail.tags.update, this.updateTags);
+ }
+
+ this.on(document, events.mail.draftReply.want, this.wantDraftReplyForMail);
+ this.on(document, events.mail.want, this.fetchSingle);
+ this.on(document, events.mail.read, this.readMail);
+ this.on(document, events.mail.unread, this.unreadMail);
+ this.on(document, events.mail.delete, this.deleteMail);
+ this.on(document, events.mail.deleteMany, this.deleteManyMails);
+ this.on(document, events.mail.recoverMany, this.recoverManyMails);
+ this.on(document, events.mail.archiveMany, this.archiveManyMails);
+ this.on(document, events.search.perform, this.newSearch);
+ this.on(document, events.ui.tag.selected, this.fetchByTag);
+ this.on(document, events.ui.tag.select, this.fetchByTag);
+ this.on(document, events.ui.mails.refresh, this.refreshMails);
+ this.on(document, events.ui.page.previous, this.previousPage);
+ this.on(document, events.ui.page.next, this.nextPage);
+
+ this.fetchByTag(null, {tag: urlParams.getTag()});
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/services/model/mail.js b/web-ui/app/js/services/model/mail.js
new file mode 100644
index 00000000..64a10c1c
--- /dev/null
+++ b/web-ui/app/js/services/model/mail.js
@@ -0,0 +1,126 @@
+/*
+ * 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/contenttype'], function (contentType) {
+ 'use strict';
+ function isSentMail() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'SENT';
+ }
+
+ function isDraftMail() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'DRAFTS';
+ }
+
+ function isInTrash() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'TRASH';
+ }
+
+ function setDraftReplyFor(ident) {
+ this.draft_reply_for = ident;
+ }
+
+ function replyToAddress() {
+ return {
+ to: [this.replying.single],
+ cc: []
+ };
+ }
+
+ function replyToAllAddress() {
+ return {
+ to: this.replying.all['to-field'],
+ cc: this.replying.all['cc-field']
+ };
+ }
+
+ function getHeadersFromMailPart (rawBody) {
+ var lines, headerLines, endOfHeaders, headers;
+
+ lines = rawBody.split('\n');
+ endOfHeaders = _.indexOf(lines, '');
+ headerLines = lines.slice(0, endOfHeaders);
+
+ headers = _.map(headerLines, function (headerLine) {
+ return _.map(headerLine.split(':'), function(elem){return elem.trim();});
+ });
+
+ return _.object(headers);
+ }
+
+ function getBodyFromMailPart (rawBody) {
+ var lines, endOfHeaders;
+
+ lines = rawBody.split('\n');
+ endOfHeaders = _.indexOf(lines, '');
+
+ return lines.slice(endOfHeaders + 1).join('\n');
+ }
+
+ function parseWithHeaders(rawBody) {
+ return {headers: getHeadersFromMailPart(rawBody), body: getBodyFromMailPart(rawBody)};
+ }
+
+ function getMailMultiParts () {
+ var mediaType = this.getMailMediaType();
+ var boundary = '--' + mediaType.params.boundary + '\n';
+ var finalBoundary = '--' + mediaType.params.boundary + '--';
+
+ var bodyParts = this.body.split(finalBoundary)[0].split(boundary);
+
+ bodyParts = _.reject(bodyParts, function(bodyPart) { return _.isEmpty(bodyPart.trim()); });
+
+ return _.map(bodyParts, parseWithHeaders);
+ }
+
+ function getMailMediaType () {
+ return new contentType.MediaType(this.header.content_type);
+ }
+
+ function isMailMultipartAlternative () {
+ return this.getMailMediaType().type === 'multipart/alternative';
+ }
+
+ function availableBodyPartsContentType () {
+ var bodyParts = this.getMailMultiParts();
+
+ return _.pluck(_.pluck(bodyParts, 'headers'), 'Content-Type');
+ }
+
+ function getMailPartByContentType (contentType) {
+ var bodyParts = this.getMailMultiParts();
+
+ return _.findWhere(bodyParts, {headers: {'Content-Type': contentType}});
+ }
+
+ return {
+ create: function (mail) {
+ if (!mail) { return; }
+
+ mail.isSentMail = isSentMail;
+ mail.isDraftMail = isDraftMail;
+ mail.isInTrash = isInTrash;
+ mail.setDraftReplyFor = setDraftReplyFor;
+ mail.replyToAddress = replyToAddress;
+ mail.replyToAllAddress = replyToAllAddress;
+ mail.getMailMediaType = getMailMediaType;
+ mail.isMailMultipartAlternative = isMailMultipartAlternative;
+ mail.getMailMultiParts = getMailMultiParts;
+ mail.availableBodyPartsContentType = availableBodyPartsContentType;
+ mail.getMailPartByContentType = getMailPartByContentType;
+ return mail;
+ }
+ };
+});
diff --git a/web-ui/app/js/services/recover_service.js b/web-ui/app/js/services/recover_service.js
new file mode 100644
index 00000000..d7d9cdc9
--- /dev/null
+++ b/web-ui/app/js/services/recover_service.js
@@ -0,0 +1,38 @@
+/*
+ * 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(function() {
+
+ this.recoverManyEmails = function (event, data) {
+ var emails = _.values(data.checkedMails);
+
+ this.trigger(document, events.mail.recoverMany, {
+ mails: emails,
+ successMessage: i18n.t('Your messages were moved to inbox!')
+ });
+
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mail.recoverMany, this.recoverManyEmails);
+ });
+
+ });
+});
diff --git a/web-ui/app/js/style_guide/main.js b/web-ui/app/js/style_guide/main.js
new file mode 100644
index 00000000..32c213cf
--- /dev/null
+++ b/web-ui/app/js/style_guide/main.js
@@ -0,0 +1,33 @@
+/*
+ * 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/>.
+ */
+$(document).ready(function(){
+ 'use strict';
+ $('a[href*=#]').click(function() {
+ if (location.pathname.replace(/^\//,'') === this.pathname.replace(/^\//,'') &&
+ location.hostname === this.hostname) {
+ var $target = $(this.hash);
+ $target = $target.length && $target ||
+ $('[name=' + this.hash.slice(1) +']');
+ if ($target.length) {
+ var targetOffset = $target.offset().top;
+ $('html,body')
+ .animate({scrollTop: targetOffset}, 500);
+ return false;
+ }
+ }
+ });
+});
diff --git a/web-ui/app/js/tags/data/tags.js b/web-ui/app/js/tags/data/tags.js
new file mode 100644
index 00000000..31703b2a
--- /dev/null
+++ b/web-ui/app/js/tags/data/tags.js
@@ -0,0 +1,66 @@
+/*
+ * 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', 'helpers/monitored_ajax', 'mixins/with_feature_toggle', 'mixins/with_auto_refresh'], function (defineComponent, events, monitoredAjax, withFeatureToggle, withAutoRefresh) {
+ 'use strict';
+
+ var DataTags = defineComponent(dataTags, withFeatureToggle('tags', function() {
+ $(document).trigger(events.ui.mails.refresh);
+ }), withAutoRefresh('refreshTags'));
+
+ DataTags.all = {
+ name: 'all',
+ ident: '8752888923742657436',
+ query: 'in:all',
+ default: true,
+ counts:{
+ total:0,
+ read:0,
+ starred:0,
+ replied:0
+ }
+ };
+
+ function dataTags() {
+ function sendTagsBackTo(on) {
+ return function(data) {
+ data.push(DataTags.all);
+ on.trigger(document, events.tags.received, {tags: data});
+ };
+ }
+
+ this.defaultAttrs({
+ tagsResource: '/tags'
+ });
+
+ this.fetchTags = function(event, params) {
+ monitoredAjax(this, this.attr.tagsResource)
+ .done(sendTagsBackTo(this));
+ };
+
+ this.refreshTags = function() {
+ var notTriggeredByEvent = null;
+ this.fetchTags(notTriggeredByEvent);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.tags.want, this.fetchTags);
+ this.on(document, events.mail.sent, this.fetchTags);
+ });
+ }
+
+ return DataTags;
+});
diff --git a/web-ui/app/js/tags/ui/tag.js b/web-ui/app/js/tags/ui/tag.js
new file mode 100644
index 00000000..37814cfc
--- /dev/null
+++ b/web-ui/app/js/tags/ui/tag.js
@@ -0,0 +1,154 @@
+/*
+ * 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',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, templates, events, i18n) {
+ 'use strict';
+
+ var Tag = defineComponent(tag);
+
+ Tag.appendedTo = function (parent, data) {
+ var res = new this();
+ res.renderAndAttach(parent, data);
+ return res;
+ };
+
+ return Tag;
+
+ function tag() {
+
+ var ALWAYS_HIDE_BADGE_FOR = ['sent', 'trash', 'all'];
+ var TOTAL_BADGE = ['drafts'];
+
+ this.displayBadge = function(tag) {
+ if(_.include(ALWAYS_HIDE_BADGE_FOR, tag.name)) { return false; }
+ if(this.badgeType(tag) === 'total') {
+ return tag.counts.total > 0;
+ } else {
+ return (tag.counts.total - tag.counts.read) > 0;
+ }
+ };
+
+ this.badgeType = function(tag) {
+ return _.include(TOTAL_BADGE, tag.name) ? 'total' : 'unread';
+ };
+
+ this.doUnselect = function () {
+ this.$node.removeClass('selected');
+ };
+
+ this.doSelect = function () {
+ this.$node.addClass('selected');
+ };
+
+ this.selectTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ if (data.tag === this.attr.tag.name) {
+ this.doSelect();
+ }
+ else {
+ this.doUnselect();
+ }
+ };
+
+ this.selectTagAll = function () {
+ this.selectTag(null, {tag: 'all'});
+ };
+
+ this.viewFor = function (tag, template, currentTag) {
+ return template({
+ tagName: tag.default ? i18n.t('tags.' + tag.name) : tag.name,
+ ident: this.hashIdent(tag.ident),
+ count: this.badgeType(tag) === 'total' ? tag.counts.total : (tag.counts.total - tag.counts.read),
+ displayBadge: this.displayBadge(tag),
+ badgeType: this.badgeType(tag),
+ icon: tag.icon,
+ selected: tag.name === currentTag ? 'selected' : ''
+ });
+ };
+
+ this.decreaseReadCountIfMatchingTag = function (ev, data) {
+ var mailbox_and_tags = _.flatten([data.tags, data.mailbox]);
+ if (_.contains(mailbox_and_tags, this.attr.tag.name)) {
+ this.attr.tag.counts.read++;
+ this.$node.html(this.viewFor(this.attr.tag, templates.tags.tagInner, this.attr.currentTag));
+ if (!_.isUndefined(this.attr.shortcut)) {
+ this.attr.shortcut.reRender();
+ }
+ }
+ };
+
+ this.triggerSelect = function () {
+ this.trigger(document, events.ui.tag.select, { tag: this.attr.tag.name });
+
+ this.removeSearchingClass();
+ };
+
+ this.addSearchingClass = function() {
+ if (this.attr.tag.name === 'all'){
+ this.$node.addClass('searching');
+ }
+ };
+
+ this.hashIdent = function(ident) {
+ if (typeof ident === 'undefined') {
+ return '';
+ }
+ if (typeof ident === 'number') {
+ return ident;
+ }
+ if (ident.match(/^[a-zA-Z0-9]+$/)) {
+ return ident;
+ }
+
+ /*jslint bitwise: true */
+ return Math.abs(String(ident).split('').reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a;},0));
+ };
+
+ this.removeSearchingClass = function() {
+ if (this.attr.tag.name === 'all'){
+ this.$node.removeClass('searching');
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.triggerSelect);
+ this.on(document, events.mail.read, this.decreaseReadCountIfMatchingTag);
+ this.on(document, events.search.perform, this.addSearchingClass);
+ this.on(document, events.search.empty, this.removeSearchingClass);
+
+ this.on(document, events.ui.tag.select, this.selectTag);
+ this.on(document, events.search.perform, this.selectTagAll);
+ this.on(document, events.search.empty, this.selectTagAll);
+ });
+
+ this.renderAndAttach = function (parent, data) {
+ var rendered = this.viewFor(data.tag, templates.tags.tag, data.currentTag);
+ parent.append(rendered);
+ this.initialize('#tag-' + this.hashIdent(data.tag.ident), data);
+ this.on(parent, events.tags.teardown, this.teardown);
+ };
+ }
+ }
+);
diff --git a/web-ui/app/js/tags/ui/tag_base.js b/web-ui/app/js/tags/ui/tag_base.js
new file mode 100644
index 00000000..9dc1ccbb
--- /dev/null
+++ b/web-ui/app/js/tags/ui/tag_base.js
@@ -0,0 +1,68 @@
+/*
+ * 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(['views/i18n', 'page/events'], function(i18n, events) {
+ 'use strict';
+
+ function tagBase() {
+ var ALWAYS_HIDE_BADGE_FOR = ['sent', 'trash', 'all'];
+ var TOTAL_BADGE = ['drafts'];
+
+ this.displayBadge = function(tag) {
+ if(_.include(ALWAYS_HIDE_BADGE_FOR, tag.name)) { return false; }
+ if(this.badgeType(tag) === 'total') {
+ return tag.counts.total > 0;
+ } else {
+ return (tag.counts.total - tag.counts.read) > 0;
+ }
+ };
+
+ this.badgeType = function(tag) {
+ return _.include(TOTAL_BADGE, tag.name) ? 'total' : 'unread';
+ };
+
+ this.doUnselect = function () {
+ this.$node.removeClass('selected');
+ };
+
+ this.doSelect = function () {
+ this.$node.addClass('selected');
+ };
+
+ this.selectTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ if (data.tag === this.attr.tag.name) {
+ this.doSelect();
+ }
+ else {
+ this.doUnselect();
+ }
+ };
+
+ this.selectTagAll = function () {
+ this.selectTag(null, {tag: 'all'});
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.tag.select, this.selectTag);
+ this.on(document, events.search.perform, this.selectTagAll);
+ this.on(document, events.search.empty, this.selectTagAll);
+ });
+ }
+
+ return tagBase;
+
+});
diff --git a/web-ui/app/js/tags/ui/tag_list.js b/web-ui/app/js/tags/ui/tag_list.js
new file mode 100644
index 00000000..a2172c6d
--- /dev/null
+++ b/web-ui/app/js/tags/ui/tag_list.js
@@ -0,0 +1,105 @@
+/*
+ * 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',
+ 'tags/ui/tag',
+ 'views/templates',
+ 'page/events',
+ 'page/router/url_params'
+ ],
+
+ function(defineComponent, Tag, templates, events, urlParams) {
+ 'use strict';
+
+ var ICON_FOR = {
+ 'inbox': 'inbox',
+ 'sent': 'send',
+ 'drafts': 'pencil',
+ 'trash': 'trash-o',
+ 'all': 'archive'
+ };
+
+ var ORDER = {
+ 'inbox': '0',
+ 'sent': '1',
+ 'drafts': '2',
+ 'trash': '3',
+ 'all': '4'
+ };
+
+ return defineComponent(tagList);
+
+ function tagOrder(nm) {
+ return ORDER[nm.name] || '999' + nm.name;
+ }
+
+ function tagList() {
+ this.defaultAttrs({
+ defaultTagList: '#default-tag-list',
+ customTagList: '#custom-tag-list'
+ });
+
+ function renderTag(tag, defaultList, customList) {
+ var list = tag.default ? defaultList : customList;
+
+ var tagComponent = Tag.appendedTo(list, {tag: tag, currentTag: this.getCurrentTag()});
+ }
+
+ function resetTagList(lists) {
+ _.each(lists, function (list) {
+ this.trigger(list, events.tags.teardown);
+ list.empty();
+ }.bind(this));
+
+ }
+
+ this.renderTagList = function(tags) {
+ var defaultList = this.select('defaultTagList');
+ var customList = this.select('customTagList');
+
+ resetTagList.call(this, [defaultList, customList]);
+
+ tags.forEach(function (tag) {
+ renderTag.call(this, tag, defaultList, customList);
+ }.bind(this));
+ };
+
+ this.displayTags = function(ev, data) {
+ this.renderTagList(_.sortBy(data.tags, tagOrder));
+ };
+
+ this.getCurrentTag = function () {
+ return this.attr.currentTag || urlParams.getTag();
+ };
+
+ this.updateCurrentTag = function(ev, data) {
+ this.attr.currentTag = data.tag;
+ };
+
+ this.renderTagListTemplate = function () {
+ this.$node.html(templates.tags.tagList());
+ };
+
+ this.after('initialize', function() {
+ this.on(document, events.tags.received, this.displayTags);
+ this.on(document, events.ui.tag.select, this.updateCurrentTag);
+ this.renderTagListTemplate();
+ });
+ }
+ }
+);
diff --git a/web-ui/app/js/user_alerts/ui/user_alerts.js b/web-ui/app/js/user_alerts/ui/user_alerts.js
new file mode 100644
index 00000000..e944a7a5
--- /dev/null
+++ b/web-ui/app/js/user_alerts/ui/user_alerts.js
@@ -0,0 +1,57 @@
+/*
+ * 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(userAlerts, withHideAndShow);
+
+ function userAlerts() {
+ this.defaultAttrs({
+ dismissTimeout: 3000
+ });
+
+ this.render = function(message) {
+ this.$node.html(templates.userAlerts.message(message));
+ this.show();
+ setTimeout(this.hide.bind(this), this.attr.dismissTimeout);
+ };
+
+
+ this.displayMessage = function(ev, data) {
+ this.render({
+ message: {
+ content: data.message,
+ class: 'message-panel__growl--' + (data.class || 'success')
+ }
+ });
+ };
+
+ this.after('initialize', function() {
+ this.on(document, events.ui.userAlerts.displayMessage, this.displayMessage);
+ });
+ }
+ }
+);
+
diff --git a/web-ui/app/js/user_settings/data/user_settings.js b/web-ui/app/js/user_settings/data/user_settings.js
new file mode 100644
index 00000000..dac29cec
--- /dev/null
+++ b/web-ui/app/js/user_settings/data/user_settings.js
@@ -0,0 +1,52 @@
+/*
+ * 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',
+ 'helpers/monitored_ajax',
+ 'page/events'
+ ],
+ function (defineComponent, monitoredAjax, events) {
+ 'use strict';
+
+ return defineComponent(function() {
+ this.defaultAttrs({
+ userSettingsResource: '/user-settings',
+ userSettings: {}
+ });
+
+ this.sendInfo = function() {
+ this.trigger(document, events.userSettings.here, this.attr.userSettings);
+ };
+
+ this.getUserSettings = function() {
+ var getUserSettingsSuccess = function (userSettings) {
+ this.attr.userSettings = userSettings;
+ };
+
+ monitoredAjax(this, this.attr.userSettingsResource, {
+ type: 'GET',
+ contentType: 'application/json; charset=utf-8'
+ }).done(getUserSettingsSuccess.bind(this));
+ };
+
+ this.after('initialize', function() {
+ this.getUserSettings();
+ this.on(document, events.userSettings.getInfo, this.sendInfo);
+ });
+ });
+});
diff --git a/web-ui/app/js/user_settings/ui/user_settings_box.js b/web-ui/app/js/user_settings/ui/user_settings_box.js
new file mode 100644
index 00000000..d3de23ed
--- /dev/null
+++ b/web-ui/app/js/user_settings/ui/user_settings_box.js
@@ -0,0 +1,77 @@
+/*
+ * 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',
+ 'features',
+ 'views/templates',
+ 'page/events',
+ 'helpers/monitored_ajax'
+ ], function (defineComponent, features, templates, events, monitoredAjax) {
+
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ close: '#user-settings-close',
+ userSettingsBoxContainer: '#user-settings-box'
+ });
+
+ this.render = function (event, userSettings) {
+ if (features.isLogoutEnabled()) {
+ this.$node.addClass('extra-bottom-space');
+ }
+
+ this.$node.addClass('arrow-box');
+ this.$node.html(templates.page.userSettingsBox(userSettings));
+
+ this.on(this.attr.close, 'click', function() {
+ this.trigger(document, events.userSettings.destroyPopup);
+ });
+
+ this.on(document, 'click', function(e) {
+ var userSettingsBoxContainer = $(this.attr.userSettingsBoxContainer).get(0);
+ var target = e.target || e.srcElement;
+
+ if (target !== userSettingsBoxContainer && !isChildOf(target, userSettingsBoxContainer)) {
+ this.destroy();
+ }
+ });
+
+ function isChildOf(child, parent) {
+ if (child.parentNode === parent) {
+ return true;
+ } else if (child.parentNode === null) {
+ return false;
+ } else {
+ return isChildOf(child.parentNode, parent);
+ }
+ }
+ };
+
+ this.destroy = function () {
+ this.$node.remove();
+ this.teardown();
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.userSettings.here, this.render);
+ this.on(document, events.userSettings.destroyPopup, this.destroy);
+ this.trigger(document, events.userSettings.getInfo);
+ });
+ });
+});
diff --git a/web-ui/app/js/user_settings/ui/user_settings_icon.js b/web-ui/app/js/user_settings/ui/user_settings_icon.js
new file mode 100644
index 00000000..a6385dc1
--- /dev/null
+++ b/web-ui/app/js/user_settings/ui/user_settings_icon.js
@@ -0,0 +1,57 @@
+/*
+ * 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',
+ 'user_settings/ui/user_settings_box'
+ ], function (defineComponent, templates, events, userSettingsBox) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ userSettingsBox: $('#user-settings-box')
+ });
+
+ this.render = function () {
+ this.$node.html(templates.page.userSettingsIcon());
+ };
+
+ this.toggleUserSettingsBox = function() {
+ if(this.attr.userSettingsBox.children().length === 0) {
+ var div = $('<div>');
+ $(this.attr.userSettingsBox).append(div);
+ userSettingsBox.attachTo(div);
+ this.attr.userSettingsInfo = userSettingsBox;
+ } else {
+ this.trigger(document, events.userSettings.destroyPopup);
+ }
+ };
+
+ this.triggerToggleUserSettingsBox = function(e) {
+ this.trigger(document, events.ui.userSettingsBox.toggle);
+ e.stopPropagation();
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on('click', this.triggerToggleUserSettingsBox);
+ this.on(document, events.ui.userSettingsBox.toggle, this.toggleUserSettingsBox);
+ });
+ });
+});
diff --git a/web-ui/app/js/views/i18n.js b/web-ui/app/js/views/i18n.js
new file mode 100644
index 00000000..29a1beca
--- /dev/null
+++ b/web-ui/app/js/views/i18n.js
@@ -0,0 +1,62 @@
+/*
+ * 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(['i18next',
+ 'i18nextXHRBackend',
+ 'i18nextBrowserLanguageDetector'],
+function(i18n, i18n_backend, I18n_detector) {
+ 'use strict';
+
+ var detector = new I18n_detector();
+ var detect = detector.detect.bind(detector);
+
+ detector.detect = function(detectionOrder) {
+ var result = detect(detectionOrder);
+ return result.replace('-', '_');
+ };
+
+ function t(i18n_key, options) {
+ var result = i18n.t(i18n_key, options);
+ var safe_string = new Handlebars.SafeString(result);
+ return safe_string.string;
+ }
+
+ function loaded(callback) {
+ i18n.on('loaded', function(loaded) {
+ callback();
+ });
+ }
+
+ function init(path) {
+ i18n
+ .use(i18n_backend)
+ .use(detector)
+ .init({
+ fallbackLng: 'en_US',
+ backend: {
+ loadPath: path + 'locales/{{lng}}/{{ns}}.json'
+ }
+ });
+ // Handlebars.registerHelper('t', self.bind(self));
+ Handlebars.registerHelper('t', t);
+ }
+
+ return {
+ t: t,
+ init: init,
+ loaded: loaded
+ };
+});
diff --git a/web-ui/app/js/views/recipientListFormatter.js b/web-ui/app/js/views/recipientListFormatter.js
new file mode 100644
index 00000000..0b887142
--- /dev/null
+++ b/web-ui/app/js/views/recipientListFormatter.js
@@ -0,0 +1,33 @@
+/*
+ * 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(function() {
+ 'use strict';
+ Handlebars.registerHelper('formatRecipients', function (header) {
+ function wrapWith(begin, end) {
+ return function (x) {
+ return begin + Handlebars.Utils.escapeExpression(x) + end;
+ };
+ }
+
+ var to = _.map(header.to, wrapWith('<span class="to">', '</span>'));
+ var cc = _.map(header.cc, wrapWith('<span class="cc">cc: ', '</span>'));
+ var bcc = _.map(header.bcc, wrapWith('<span class="bcc">bcc: ', '</span>'));
+
+ return new Handlebars.SafeString(to.concat(cc, bcc).join(', '));
+ });
+});
diff --git a/web-ui/app/js/views/templates.js b/web-ui/app/js/views/templates.js
new file mode 100644
index 00000000..d4185471
--- /dev/null
+++ b/web-ui/app/js/views/templates.js
@@ -0,0 +1,85 @@
+/*
+ * 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(['hbs/templates'], function (templates) {
+ 'use strict';
+
+ var Templates = {
+ compose: {
+ box: window.Pixelated['app/templates/compose/compose_box.hbs'],
+ inlineBox: window.Pixelated['app/templates/compose/inline_box.hbs'],
+ replySection: window.Pixelated['app/templates/compose/reply_section.hbs'],
+ recipientInput: window.Pixelated['app/templates/compose/recipient_input.hbs'],
+ fixedRecipient: window.Pixelated['app/templates/compose/fixed_recipient.hbs'],
+ recipients: window.Pixelated['app/templates/compose/recipients.hbs'],
+ feedback: window.Pixelated['app/templates/compose/feedback_box.hbs'],
+ attachmentsList: window.Pixelated['app/templates/compose/attachments_list.hbs'],
+ attachmentItem: window.Pixelated['app/templates/compose/attachment_item.hbs'],
+ attachmentUploadItem: window.Pixelated['app/templates/compose/attachment_upload_item.hbs'],
+ uploadAttachmentFailed: window.Pixelated['app/templates/compose/upload_attachment_failed.hbs']
+ },
+ tags: {
+ tagList: window.Pixelated['app/templates/tags/tag_list.hbs'],
+ tag: window.Pixelated['app/templates/tags/tag.hbs'],
+ tagInner: window.Pixelated['app/templates/tags/tag_inner.hbs'],
+ shortcut: window.Pixelated['app/templates/tags/shortcut.hbs']
+ },
+ userAlerts: {
+ message: window.Pixelated['app/templates/user_alerts/message.hbs']
+ },
+ mails: {
+ single: window.Pixelated['app/templates/mails/single.hbs'],
+ fullView: window.Pixelated['app/templates/mails/full_view.hbs'],
+ mailActions: window.Pixelated['app/templates/mails/mail_actions.hbs'],
+ draft: window.Pixelated['app/templates/mails/draft.hbs'],
+ sent: window.Pixelated['app/templates/mails/sent.hbs'],
+ trash: window.Pixelated['app/templates/mails/trash.hbs']
+ },
+ mailActions: {
+ actionsBox: window.Pixelated['app/templates/mail_actions/actions_box.hbs'],
+ trashActionsBox: window.Pixelated['app/templates/mail_actions/trash_actions_box.hbs'],
+ composeTrigger: window.Pixelated['app/templates/mail_actions/compose_trigger.hbs'],
+ refreshTrigger: window.Pixelated['app/templates/mail_actions/refresh_trigger.hbs'],
+ paginationTrigger: window.Pixelated['app/templates/mail_actions/pagination_trigger.hbs']
+ },
+ noMessageSelected: window.Pixelated['app/templates/compose/no_message_selected.hbs'],
+ noMailsAvailable: window.Pixelated['app/templates/compose/no_mails_available.hbs'],
+ search: {
+ trigger: window.Pixelated['app/templates/search/search_trigger.hbs']
+ },
+ page: {
+ userSettingsIcon: window.Pixelated['app/templates/page/user_settings_icon.hbs'],
+ userSettingsBox: window.Pixelated['app/templates/page/user_settings_box.hbs'],
+ logout: window.Pixelated['app/templates/page/logout.hbs'],
+ logoutShortcut: window.Pixelated['app/templates/page/logout_shortcut.hbs'],
+ version: window.Pixelated['app/templates/page/version.hbs']
+ },
+ feedback: {
+ feedback: window.Pixelated['app/templates/feedback/feedback_trigger.hbs']
+ }
+ };
+
+ Handlebars.registerPartial('tag_inner', Templates.tags.tagInner);
+ Handlebars.registerPartial('recipients', Templates.compose.recipients);
+ Handlebars.registerPartial('attachments_list', Templates.compose.attachmentsList);
+ Handlebars.registerPartial('attachments_upload', Templates.compose.attachmentsList);
+ Handlebars.registerPartial('attachment_item', Templates.compose.attachmentItem);
+ Handlebars.registerPartial('attachment_upload_item', Templates.compose.attachmentUploadItem);
+ Handlebars.registerPartial('uploadAttachmentFailed', Templates.compose.uploadAttachmentFailed);
+
+ return Templates;
+});