summaryrefslogtreecommitdiff
path: root/web-ui/app
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
parent521bce7eff5cf921156efe74c91a0499ade43619 (diff)
Revert "[#801] Merge branch 'signup'"
This reverts commit d10f607a4d40587510b0dc31b31fe4750bf4a3a3, reversing changes made to c28abba2f5b1186c671ebef508d40ffaae6d5bc5.
Diffstat (limited to 'web-ui/app')
-rw-r--r--web-ui/app/404.html157
-rw-r--r--web-ui/app/favicon.ico0
-rw-r--r--web-ui/app/fonts/OpenSans-Bold.woffbin0 -> 14504 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-BoldItalic.woffbin0 -> 15488 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-Extrabold.woffbin0 -> 15312 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-ExtraboldItalic.woffbin0 -> 15932 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-Italic.woffbin0 -> 15768 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-Light.woffbin0 -> 15048 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-Semibold.woffbin0 -> 15236 bytes
-rw-r--r--web-ui/app/fonts/OpenSans-SemiboldItalic.woffbin0 -> 15736 bytes
-rw-r--r--web-ui/app/fonts/OpenSans.woffbin0 -> 14604 bytes
-rw-r--r--web-ui/app/fonts/OpenSansLight-Italic.woffbin0 -> 15956 bytes
-rw-r--r--web-ui/app/fonts/icomoon.ttfbin0 -> 1272 bytes
-rw-r--r--web-ui/app/fonts/icomoon.woffbin0 -> 1348 bytes
-rw-r--r--web-ui/app/images/LOADING-transparent.gifbin0 -> 16170 bytes
-rw-r--r--web-ui/app/images/fa-sent.svg15
-rw-r--r--web-ui/app/images/favicon.pngbin0 -> 592 bytes
-rw-r--r--web-ui/app/images/logo.svg30
-rw-r--r--web-ui/app/images/pixelated-symbol-blue-transparent-01.pngbin0 -> 9075 bytes
-rw-r--r--web-ui/app/index.html113
-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
-rw-r--r--web-ui/app/locales/en_US/translation.json72
-rw-r--r--web-ui/app/locales/pt_BR/translation.json72
-rw-r--r--web-ui/app/locales/sv_SE/translation.json42
-rw-r--r--web-ui/app/robots.txt3
-rw-r--r--web-ui/app/sandbox.html16
-rw-r--r--web-ui/app/scss/_mixins.scss71
-rw-r--r--web-ui/app/scss/_others.scss72
-rw-r--r--web-ui/app/scss/base/_colors.scss64
-rw-r--r--web-ui/app/scss/base/_fonts.scss68
-rw-r--r--web-ui/app/scss/base/_scaffolding.scss10
-rw-r--r--web-ui/app/scss/mixins/_position-helpers.scss9
-rw-r--r--web-ui/app/scss/mixins/_tags.scss110
-rw-r--r--web-ui/app/scss/sandbox.scss27
-rw-r--r--web-ui/app/scss/style.scss39
-rw-r--r--web-ui/app/scss/templates/_no-content-placeholder.scss5
-rw-r--r--web-ui/app/scss/templates/_unread-count.scss14
-rw-r--r--web-ui/app/scss/vendor/_customfont.scss9
-rw-r--r--web-ui/app/scss/vendor/_foundation.scss2066
-rw-r--r--web-ui/app/scss/vendor/_reset.scss421
-rw-r--r--web-ui/app/scss/vendor/_scut.scss1518
-rw-r--r--web-ui/app/scss/views/_action-bar.scss159
-rw-r--r--web-ui/app/scss/views/_close-button.scss22
-rw-r--r--web-ui/app/scss/views/_compose-button.scss27
-rw-r--r--web-ui/app/scss/views/_compose-view.scss451
-rw-r--r--web-ui/app/scss/views/_mail-list.scss124
-rw-r--r--web-ui/app/scss/views/_message-panel.scss26
-rw-r--r--web-ui/app/scss/views/_navigation.scss589
-rw-r--r--web-ui/app/scss/views/_no-mails-available.scss3
-rw-r--r--web-ui/app/scss/views/_no-message-selected.scss14
-rw-r--r--web-ui/app/scss/views/_read-view.scss165
-rw-r--r--web-ui/app/scss/views/_security-labels.scss67
-rw-r--r--web-ui/app/templates/compose/attachment_item.hbs4
-rw-r--r--web-ui/app/templates/compose/attachment_upload_item.hbs5
-rw-r--r--web-ui/app/templates/compose/attachments_list.hbs14
-rw-r--r--web-ui/app/templates/compose/compose_box.hbs32
-rw-r--r--web-ui/app/templates/compose/feedback_box.hbs18
-rw-r--r--web-ui/app/templates/compose/fixed_recipient.hbs8
-rw-r--r--web-ui/app/templates/compose/inline_box.hbs20
-rw-r--r--web-ui/app/templates/compose/no_mails_available.hbs7
-rw-r--r--web-ui/app/templates/compose/no_message_selected.hbs3
-rw-r--r--web-ui/app/templates/compose/recipient_input.hbs1
-rw-r--r--web-ui/app/templates/compose/recipients.hbs33
-rw-r--r--web-ui/app/templates/compose/reply_section.hbs6
-rw-r--r--web-ui/app/templates/compose/upload_attachment_failed.hbs6
-rw-r--r--web-ui/app/templates/feedback/feedback_trigger.hbs8
-rw-r--r--web-ui/app/templates/mail_actions/actions_box.hbs7
-rw-r--r--web-ui/app/templates/mail_actions/compose_trigger.hbs3
-rw-r--r--web-ui/app/templates/mail_actions/pagination_trigger.hbs3
-rw-r--r--web-ui/app/templates/mail_actions/refresh_trigger.hbs3
-rw-r--r--web-ui/app/templates/mail_actions/trash_actions_box.hbs5
-rw-r--r--web-ui/app/templates/mails/draft.hbs41
-rw-r--r--web-ui/app/templates/mails/full_view.hbs83
-rw-r--r--web-ui/app/templates/mails/mail_actions.hbs6
-rw-r--r--web-ui/app/templates/mails/sent.hbs36
-rw-r--r--web-ui/app/templates/mails/single.hbs28
-rw-r--r--web-ui/app/templates/mails/trash.hbs32
-rw-r--r--web-ui/app/templates/page/logout.hbs9
-rw-r--r--web-ui/app/templates/page/logout_shortcut.hbs6
-rw-r--r--web-ui/app/templates/page/user_settings_box.hbs10
-rw-r--r--web-ui/app/templates/page/user_settings_icon.hbs8
-rw-r--r--web-ui/app/templates/page/version.hbs2
-rw-r--r--web-ui/app/templates/search/search_trigger.hbs3
-rw-r--r--web-ui/app/templates/tags/shortcut.hbs9
-rw-r--r--web-ui/app/templates/tags/tag.hbs3
-rw-r--r--web-ui/app/templates/tags/tag_inner.hbs4
-rw-r--r--web-ui/app/templates/tags/tag_list.hbs6
-rw-r--r--web-ui/app/templates/user_alerts/message.hbs1
183 files changed, 15587 insertions, 0 deletions
diff --git a/web-ui/app/404.html b/web-ui/app/404.html
new file mode 100644
index 00000000..fdace4ab
--- /dev/null
+++ b/web-ui/app/404.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Page Not Found :(</title>
+ <style>
+ ::-moz-selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+
+ ::selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+
+ html {
+ padding: 30px 10px;
+ font-size: 20px;
+ line-height: 1.4;
+ color: #737373;
+ background: #f0f0f0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ html,
+ input {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ }
+
+ body {
+ max-width: 500px;
+ _width: 500px;
+ padding: 30px 20px 50px;
+ border: 1px solid #b3b3b3;
+ border-radius: 4px;
+ margin: 0 auto;
+ box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
+ background: #fcfcfc;
+ }
+
+ h1 {
+ margin: 0 10px;
+ font-size: 50px;
+ text-align: center;
+ }
+
+ h1 span {
+ color: #bbb;
+ }
+
+ h3 {
+ margin: 1.5em 0 0.5em;
+ }
+
+ p {
+ margin: 1em 0;
+ }
+
+ ul {
+ padding: 0 0 0 40px;
+ margin: 1em 0;
+ }
+
+ .container {
+ max-width: 380px;
+ _width: 380px;
+ margin: 0 auto;
+ }
+
+ /* google search */
+
+ #goog-fixurl ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ #goog-fixurl form {
+ margin: 0;
+ }
+
+ #goog-wm-qt,
+ #goog-wm-sb {
+ border: 1px solid #bbb;
+ font-size: 16px;
+ line-height: normal;
+ vertical-align: top;
+ color: #444;
+ border-radius: 2px;
+ }
+
+ #goog-wm-qt {
+ width: 220px;
+ height: 20px;
+ padding: 5px;
+ margin: 5px 10px 0 0;
+ box-shadow: inset 0 1px 1px #ccc;
+ }
+
+ #goog-wm-sb {
+ display: inline-block;
+ height: 32px;
+ padding: 0 10px;
+ margin: 5px 0 0;
+ white-space: nowrap;
+ cursor: pointer;
+ background-color: #f5f5f5;
+ background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ *overflow: visible;
+ *display: inline;
+ *zoom: 1;
+ }
+
+ #goog-wm-sb:hover,
+ #goog-wm-sb:focus {
+ border-color: #aaa;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ background-color: #f8f8f8;
+ }
+
+ #goog-wm-qt:hover,
+ #goog-wm-qt:focus {
+ border-color: #105cb6;
+ outline: 0;
+ color: #222;
+ }
+
+ input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <h1>Not found <span>:(</span></h1>
+ <p>Sorry, but the page you were trying to view does not exist.</p>
+ <p>It looks like this was the result of either:</p>
+ <ul>
+ <li>a mistyped address</li>
+ <li>an out-of-date link</li>
+ </ul>
+ <script>
+ var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
+ </script>
+ <script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
+ </div>
+ </body>
+</html>
diff --git a/web-ui/app/favicon.ico b/web-ui/app/favicon.ico
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/web-ui/app/favicon.ico
diff --git a/web-ui/app/fonts/OpenSans-Bold.woff b/web-ui/app/fonts/OpenSans-Bold.woff
new file mode 100644
index 00000000..dacf3c9c
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-Bold.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-BoldItalic.woff b/web-ui/app/fonts/OpenSans-BoldItalic.woff
new file mode 100644
index 00000000..a4e29c0f
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-BoldItalic.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-Extrabold.woff b/web-ui/app/fonts/OpenSans-Extrabold.woff
new file mode 100644
index 00000000..7a2e352b
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-Extrabold.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff b/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff
new file mode 100644
index 00000000..ce3ab2e7
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-Italic.woff b/web-ui/app/fonts/OpenSans-Italic.woff
new file mode 100644
index 00000000..c5f6bac1
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-Italic.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-Light.woff b/web-ui/app/fonts/OpenSans-Light.woff
new file mode 100644
index 00000000..eb601d70
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-Light.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-Semibold.woff b/web-ui/app/fonts/OpenSans-Semibold.woff
new file mode 100644
index 00000000..56c44944
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-Semibold.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans-SemiboldItalic.woff b/web-ui/app/fonts/OpenSans-SemiboldItalic.woff
new file mode 100644
index 00000000..3a439fc3
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans-SemiboldItalic.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSans.woff b/web-ui/app/fonts/OpenSans.woff
new file mode 100644
index 00000000..77706fa6
--- /dev/null
+++ b/web-ui/app/fonts/OpenSans.woff
Binary files differ
diff --git a/web-ui/app/fonts/OpenSansLight-Italic.woff b/web-ui/app/fonts/OpenSansLight-Italic.woff
new file mode 100644
index 00000000..3f9f088f
--- /dev/null
+++ b/web-ui/app/fonts/OpenSansLight-Italic.woff
Binary files differ
diff --git a/web-ui/app/fonts/icomoon.ttf b/web-ui/app/fonts/icomoon.ttf
new file mode 100644
index 00000000..61315d04
--- /dev/null
+++ b/web-ui/app/fonts/icomoon.ttf
Binary files differ
diff --git a/web-ui/app/fonts/icomoon.woff b/web-ui/app/fonts/icomoon.woff
new file mode 100644
index 00000000..82f11748
--- /dev/null
+++ b/web-ui/app/fonts/icomoon.woff
Binary files differ
diff --git a/web-ui/app/images/LOADING-transparent.gif b/web-ui/app/images/LOADING-transparent.gif
new file mode 100644
index 00000000..ac9abcde
--- /dev/null
+++ b/web-ui/app/images/LOADING-transparent.gif
Binary files differ
diff --git a/web-ui/app/images/fa-sent.svg b/web-ui/app/images/fa-sent.svg
new file mode 100644
index 00000000..a4b4bea4
--- /dev/null
+++ b/web-ui/app/images/fa-sent.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="435px" height="459px" viewBox="0 0 435 459" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>fa-sent</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Desktop-HD" transform="translate(-503.000000, -283.000000)" fill="#030404">
+ <g id="fa-sent" transform="translate(503.000000, 283.000000)">
+ <path d="M112.1182,211.1919 L57.1442,291.5079 C56.9542,291.8879 56.7212,292.6389 56.4372,293.7709 C56.1542,294.9019 55.9162,295.6579 55.7302,296.0339 L145.1252,296.0339 L172.0002,350.3489 L262.5252,350.3489 L289.4002,296.0339 L378.7952,296.0339 C378.6052,295.4679 378.3712,294.7169 378.0882,293.7709 C377.8052,292.8289 377.5662,292.0729 377.3812,291.5079 L320.4072,211.1919 L318.6922,170.0329 C318.6922,170.0329 319.1052,162.5819 322.1052,159.7069 C325.3492,156.5979 334.9462,156.8769 334.9462,156.8769 C339.8482,156.8769 344.8002,158.4809 349.7992,161.6859 C354.7942,164.8949 358.2372,168.8549 360.1242,173.5669 L427.4532,269.7249 C432.1652,281.2309 434.5252,292.8289 434.5252,304.5209 L434.5252,440.8759 C434.5252,445.7819 432.7302,450.0259 429.1502,453.6059 C425.5652,457.1909 421.3222,458.9809 416.4202,458.9809 L18.1052,458.9809 C13.1992,458.9809 8.9552,457.1909 5.3752,453.6059 C1.7902,450.0259 0.0002,445.7819 0.0002,440.8759 L0.0002,304.5209 C0.0002,292.8289 2.3562,281.2309 7.0722,269.7249 L74.4012,173.5669 C76.2842,168.8549 79.7272,164.8949 84.7262,161.6859 C89.7222,158.4809 94.6722,156.8769 99.5792,156.8769 C99.5792,156.8769 111.4732,156.0469 113.3482,158.5469 C114.5392,160.1349 114.3902,164.7829 114.3902,164.7829 L112.1182,211.1919 Z" id="Fill-1"></path>
+ <path d="M227.7715,4.3461 C221.9715,-1.4489 212.5765,-1.4489 206.7815,4.3461 L121.0585,90.0691 C115.2595,95.8681 113.0405,111.0631 128.2935,111.0631 L166.8865,111.0731 L166.8865,244.3991 C166.8865,252.7301 173.6405,259.4891 181.9725,259.4891 L252.3855,259.4891 C260.7175,259.4891 267.4765,252.7351 267.4765,244.3991 L267.4765,111.0781 L306.2595,111.0681 C321.4265,111.0681 319.2935,95.8731 313.4945,90.0731 L227.7715,4.3461 Z" id="Fill-4"></path>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/images/favicon.png b/web-ui/app/images/favicon.png
new file mode 100644
index 00000000..e14841c7
--- /dev/null
+++ b/web-ui/app/images/favicon.png
Binary files differ
diff --git a/web-ui/app/images/logo.svg b/web-ui/app/images/logo.svg
new file mode 100644
index 00000000..6c2d8989
--- /dev/null
+++ b/web-ui/app/images/logo.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="30.4 627.3 612 171.1" enable-background="new 30.4 627.3 612 171.1" xml:space="preserve">
+<g>
+ <path fill="#3E3B38" d="M30.4,669.9v85.8l75.5,42.7l75.5-42.7v-85.8l-75.5-42.7L30.4,669.9z M102.7,767.4l-44-24.3v-52.6l44,25
+ V767.4z M152.5,743l-43.4,24.3v-51.8l43.4-25.4V743z M152.5,683.1l-46.7,27.8l-47.2-27.8l47.2-25.4L152.5,683.1z"/>
+ <path fill="#3E3B38" d="M233.8,678.3h-24v71.2h16.2v-26.5h7.8c14,0,24.3-8,24.3-22.9C258.1,685.6,247.6,678.3,233.8,678.3z
+ M230.6,710.2h-4.6v-18.8h4.6c6.5,0,12.5,2.2,12.5,9.5C243,708.1,237.1,710.2,230.6,710.2z"/>
+ <rect x="263.5" y="678.3" fill="#3E3B38" width="16.2" height="71.2"/>
+ <polygon fill="#3E3B38" points="350.4,678.3 330.1,678.3 316.9,697.7 303.7,678.3 284.6,678.3 307,711.1 282.6,749.5 302.9,749.5
+ 316.9,725.3 331,749.5 352.1,749.5 326.9,711.1 "/>
+ <polygon fill="#3E3B38" points="354.7,749.5 395.5,749.5 395.5,735.2 370.9,735.2 370.9,721 394.4,721 394.4,706.6 370.9,706.6
+ 370.9,692.5 395.5,692.5 395.5,678.3 354.7,678.3 "/>
+ <path fill="#3E3B38" d="M456.1,678.3l-22.9,57h-15.9v-57h-16.2v71.2h26.5h14.3h3.2l5.4-14.3h27l5.4,14.3h17.5l-28.9-71.2H456.1z
+ M455.7,721l7.8-20.7h0.2l7.8,20.7H455.7z"/>
+ <polygon fill="#3E3B38" points="486.4,692.5 503.4,692.5 503.4,749.5 519.6,749.5 519.6,692.5 536.6,692.5 536.6,678.3
+ 486.4,678.3 "/>
+ <polygon fill="#3E3B38" points="542,749.5 582.8,749.5 582.8,735.2 558.4,735.2 558.4,721 581.9,721 581.9,706.6 558.4,706.6
+ 558.4,692.5 582.8,692.5 582.8,678.3 542,678.3 "/>
+ <path fill="#3E3B38" d="M606.5,678.3h-17.9v71.2h17.9c19.7,0,35.9-14.9,35.9-35.6C642.4,693.1,625.9,678.3,606.5,678.3z M607,735
+ h-2.4v-42.1h2.4c12.1,0,20.3,9.1,20.3,21.1C627.3,725.8,619.1,735,607,735z"/>
+</g>
+<polygon id="clock1" fill="#3E3B38" points="105.8,657.8 105.8,628 105.8,627.3 181.4,669.9 152.5,683.1 "/>
+<polygon id="clock2" fill="#3E3B38" points="152.5,683.1 181.4,669.9 181.4,755.7 152.5,743 "/>
+<polygon id="clock3" fill="#3E3B38" points="105.9,798.3 105.9,769 152.5,743 181.4,755.7 "/>
+<polygon id="clock4" fill="#3E3B38" points="58.7,743.1 105.9,769 105.9,798.3 30.4,755.7 "/>
+<polygon id="clock5" fill="#3E3B38" points="30.4,669.9 58.6,683.1 58.7,743.1 30.4,755.7 "/>
+<polygon id="clock6" fill="#3E3B38" points="105.8,628 105.8,657.8 58.6,683.1 30.4,669.9 105.8,627.3 "/>
+</svg>
diff --git a/web-ui/app/images/pixelated-symbol-blue-transparent-01.png b/web-ui/app/images/pixelated-symbol-blue-transparent-01.png
new file mode 100644
index 00000000..96b92155
--- /dev/null
+++ b/web-ui/app/images/pixelated-symbol-blue-transparent-01.png
Binary files differ
diff --git a/web-ui/app/index.html b/web-ui/app/index.html
new file mode 100644
index 00000000..4b6a81a0
--- /dev/null
+++ b/web-ui/app/index.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html>
+<head>
+<link rel="icon" type="image/png" href="assets/images/Favicon.png">
+<meta charset="utf-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+<title>$account_email - Pixelated Mail</title>
+<meta name="description" content="">
+<meta name="viewport" content="width=device-width">
+<link href="assets/bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css">
+<link href="assets/bower_components/jquery-file-upload/css/jquery.fileupload.css" rel="stylesheet" type="text/css">
+<link rel="stylesheet" href="assets/css/style.css">
+</head>
+
+<body>
+<div class="off-canvas-wrap move-right menu" data-offcanvas>
+ <div class="inner-wrap">
+ <section id="left-pane" class="left-off-canvas-menu">
+ <a class="left-off-canvas-logo side-nav-toggle" href="#">
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="30.4 627.3 612 171.1" enable-background="new 30.4 627.3 612 171.1" xml:space="preserve">
+<g>
+ <path fill="#3E3B38" id="pix-logo" d="M30.4,669.9v85.8l75.5,42.7l75.5-42.7v-85.8l-75.5-42.7L30.4,669.9z M102.7,767.4l-44-24.3v-52.6l44,25
+ V767.4z M152.5,743l-43.4,24.3v-51.8l43.4-25.4V743z M152.5,683.1l-46.7,27.8l-47.2-27.8l47.2-25.4L152.5,683.1z"/>
+ <path fill="#3E3B38" d="M233.8,678.3h-24v71.2h16.2v-26.5h7.8c14,0,24.3-8,24.3-22.9C258.1,685.6,247.6,678.3,233.8,678.3z
+ M230.6,710.2h-4.6v-18.8h4.6c6.5,0,12.5,2.2,12.5,9.5C243,708.1,237.1,710.2,230.6,710.2z"/>
+ <rect x="263.5" y="678.3" fill="#3E3B38" width="16.2" height="71.2"/>
+ <polygon fill="#3E3B38" points="350.4,678.3 330.1,678.3 316.9,697.7 303.7,678.3 284.6,678.3 307,711.1 282.6,749.5 302.9,749.5
+ 316.9,725.3 331,749.5 352.1,749.5 326.9,711.1 "/>
+ <polygon fill="#3E3B38" points="354.7,749.5 395.5,749.5 395.5,735.2 370.9,735.2 370.9,721 394.4,721 394.4,706.6 370.9,706.6
+ 370.9,692.5 395.5,692.5 395.5,678.3 354.7,678.3 "/>
+ <path fill="#3E3B38" d="M456.1,678.3l-22.9,57h-15.9v-57h-16.2v71.2h26.5h14.3h3.2l5.4-14.3h27l5.4,14.3h17.5l-28.9-71.2H456.1z
+ M455.7,721l7.8-20.7h0.2l7.8,20.7H455.7z"/>
+ <polygon fill="#3E3B38" points="486.4,692.5 503.4,692.5 503.4,749.5 519.6,749.5 519.6,692.5 536.6,692.5 536.6,678.3
+ 486.4,678.3 "/>
+ <polygon fill="#3E3B38" points="542,749.5 582.8,749.5 582.8,735.2 558.4,735.2 558.4,721 581.9,721 581.9,706.6 558.4,706.6
+ 558.4,692.5 582.8,692.5 582.8,678.3 542,678.3 "/>
+ <path fill="#3E3B38" d="M606.5,678.3h-17.9v71.2h17.9c19.7,0,35.9-14.9,35.9-35.6C642.4,693.1,625.9,678.3,606.5,678.3z M607,735
+ h-2.4v-42.1h2.4c12.1,0,20.3,9.1,20.3,21.1C627.3,725.8,619.1,735,607,735z"/>
+ </g>
+ <polygon id="clock1" class="logo-part-animation-off" fill="#3E3B38" points="105.8,657.8 105.8,628 105.8,627.3 181.4,669.9 152.5,683.1 "/>
+ <polygon id="clock2" class="logo-part-animation-off" fill="#3E3B38" points="152.5,683.1 181.4,669.9 181.4,755.7 152.5,743 "/>
+ <polygon id="clock3" class="logo-part-animation-off" fill="#3E3B38" points="105.9,798.3 105.9,769 152.5,743 181.4,755.7 "/>
+ <polygon id="clock4" class="logo-part-animation-off" fill="#3E3B38" points="58.7,743.1 105.9,769 105.9,798.3 30.4,755.7 "/>
+ <polygon id="clock5" class="logo-part-animation-off" fill="#3E3B38" points="30.4,669.9 58.6,683.1 58.7,743.1 30.4,755.7 "/>
+ <polygon id="clock6" class="logo-part-animation-off" fill="#3E3B38" points="105.8,628 105.8,657.8 58.6,683.1 30.4,669.9 105.8,627.3 "/>
+ </svg>
+ </a>
+ <a class="side-nav-toggle side-nav-toggle-icon" href="#">
+ <i class="toggle fa fa-navicon"></i>
+ </a>
+ <nav id="tag-list"></nav>
+ <div class="side-nav-bottom">
+ <div class="version">0.3.1-beta</div>
+ <nav id="feedback"></nav>
+ <nav id="user-settings-icon"></nav>
+ <nav id="logout"></nav>
+ </div>
+ </section>
+ </div>
+</div>
+
+<div class="off-canvas-wrap content" data-offcanvas>
+ <header class="message-panel-container" >
+ <div id="user-alerts" class="message-panel"></div>
+ </header>
+
+ <div class="inner-wrap">
+ <a class="left-off-canvas-toggle" href="#">
+ </a>
+ <article id='middle-pane-container' class="small-5 medium-5 large-5 columns no-padding">
+ <section id="top-pane" class="small-12 large-12 no-padding">
+ <div id="compose-search-trigger">
+ <div id="compose" class="column small-12 large-4 no-padding">
+ <div id="compose-trigger"></div>
+ </div>
+ <div id="search-trigger" class="small-12 large-8 columns no-padding">
+ </div>
+ </div>
+ <ul id="list-actions"></ul>
+ </section>
+
+ <section id="middle-pane" class="small-9 medium-12 large-12 columns no-padding">
+ <ul id="mail-list">
+ </ul>
+ <div id="user-settings-box"></div>
+ </section>
+ </article>
+
+ <section id="right-pane" class="small-7 medium-7 large-7 columns">
+ </section>
+ </div>
+</div>
+
+<!--usemin_start-->
+<script src="assets/bower_components/modernizr/modernizr.js"></script>
+<script src="assets/bower_components/lodash/dist/lodash.js"></script>
+<script src="assets/bower_components/jquery/dist/jquery.js"></script>
+<script src="assets/bower_components/jquery-ui/jquery-ui.min.js"></script>
+<script src="assets/bower_components/jquery-file-upload/js/jquery.fileupload.js"></script>
+<script src="assets/js/lib/highlightRegex.js"></script>
+<script src="assets/bower_components/handlebars/handlebars.min.js"></script>
+<script src="assets/bower_components/typeahead.js/dist/typeahead.bundle.min.js"></script>
+<script src="assets/bower_components/foundation/js/foundation.js" ></script>
+<script src="assets/bower_components/foundation/js/foundation/foundation.reveal.js" ></script>
+<script src="assets/bower_components/foundation/js/foundation/foundation.offcanvas.js"></script>
+<script src="assets/js/foundation/initialize_foundation.js"></script>
+<script src="assets/bower_components/iframe-resizer/js/iframeResizer.min.js"></script>
+<script src="assets/bower_components/requirejs/require.js" data-main="assets/js/main.js"></script>
+<!--usemin_end-->
+
+</body>
+</html>
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;
+});
diff --git a/web-ui/app/locales/en_US/translation.json b/web-ui/app/locales/en_US/translation.json
new file mode 100644
index 00000000..3e006156
--- /dev/null
+++ b/web-ui/app/locales/en_US/translation.json
@@ -0,0 +1,72 @@
+{
+ "compose": "Compose",
+ "re": "Re",
+ "fwd": "Fwd",
+ "trash-single": "Your message was moved to trash!",
+ "trash-bulk": "Your messages were moved to trash!",
+ "your-message-was-archived": "Your message was archived",
+ "delete-single": "Your message was permanently deleted!",
+ "delete-bulk": "Your messages were permanently deleted!",
+ "draft-saving": "Saving to Drafts...",
+ "draft-saved": "Draft saved",
+ "recipients-not-valid": "One or more of the recipients are not valid emails",
+ "failed-change-tags": "Could not change mail tags",
+ "invalid-tag-name": "Invalid tag name",
+ "failed-delete-single": "Could not delete email",
+ "failed-delete-bulk": "Could not delete emails",
+ "failed-fetch-messages": "Could not fetch messages",
+ "failed-archive": "Could not archive emails",
+ "to": "to",
+ "cc": "CC",
+ "bcc": "BCC",
+ "body": "Body",
+ "subject": "Subject",
+ "send": "Send",
+ "reply": "Reply",
+ "reply-to-all": "Reply to all",
+ "delete-this-message": "Delete this message",
+ "mark-as-read": "Mark as read",
+ "mark-as-unread": "Mark as unread",
+ "delete": "Delete",
+ "archive": "Archive",
+ "nothing-selected": "Nothing selected",
+ "add-tag-placeholder": "Press Enter to add tag",
+ "no-subject": "<No subject>",
+ "no-recipient": "<No recipients>",
+ "you": "you",
+ "encrypted": "Encrypted",
+ "not-encrypted": "Not encrypted",
+ "signed": "Verified sender",
+ "not-signed": "Not signed",
+ "sending-mail": "Sending...",
+ "trash-button": "Delete it",
+ "search-placeholder" : "Search...",
+ "search-results-for": "Search results for",
+ "forward": "Forward",
+ "feedback-placeholder": "Tell us what you liked, didn't like, what is missing and generally what you think about Pixelated.",
+ "user-account": "My Account",
+ "email-address": "Email address",
+ "public-key-fingerprint": "Public key fingerprint",
+ "version": "version",
+ "logout": "Logout",
+ "delete-permanently": "Delete Permanently",
+ "move-to-inbox": "Move to Inbox",
+ "reply-author-line": "On {{date}}, <{{from}}> wrote:\n",
+ "refresh": "refresh",
+ "click-to-remove": "Click to remove",
+ "no-results-for": "No results for",
+ "no-emails-in": "No emails in",
+ "error": {
+ "timeout": "A timeout occurred",
+ "general": "Problems talking to server",
+ "parse": "Got invalid response from server"
+ },
+ "tags": {
+ "inbox": "Inbox",
+ "sent": "Sent",
+ "drafts": "Drafts",
+ "trash": "Trash",
+ "all": "All",
+ "tags": "Tags"
+ }
+}
diff --git a/web-ui/app/locales/pt_BR/translation.json b/web-ui/app/locales/pt_BR/translation.json
new file mode 100644
index 00000000..ff766a2b
--- /dev/null
+++ b/web-ui/app/locales/pt_BR/translation.json
@@ -0,0 +1,72 @@
+{
+ "compose": "Escrever",
+ "re": "Res",
+ "fwd": "Enc",
+ "trash-single": "Sua mensagem foi movida para a lixeira!",
+ "trash-bulk": "Suas mensagens foram movidas para a lixeira!",
+ "your-message-was-archived": "Sua mensagem foi arquivada",
+ "delete-single": "Sua mensagem foi permanentemente deletada!",
+ "delete-bulk": "Suas mensagens foram permanentemente deletadas!",
+ "draft-saving": "Salvando rascunho...",
+ "draft-saved": "Rascunho salvo",
+ "recipients-not-valid": "Um ou mais destinatários não são emails válidos",
+ "failed-change-tags": "Não pode atualizar as tags do email",
+ "invalid-tag-name": "Nome inválido para tag",
+ "failed-delete-single": "Não pode deletar o email",
+ "failed-delete-bulk": "Não foi possível remover os emails",
+ "failed-fetch-messages": "Não pode receber as mensagens",
+ "failed-archive": "Não foi possível arquivar os emails",
+ "to": "para",
+ "cc": "CC",
+ "bcc": "CCO",
+ "body": "Mensagem",
+ "subject": "Assunto",
+ "send": "Enviar",
+ "reply": "Responder",
+ "reply-to-all": "Responder para todos",
+ "delete-this-message": "Deletar essa mensagem",
+ "mark-as-read": "Marcar como lida",
+ "mark-as-unread": "Marcar como não lida",
+ "delete": "Deletar",
+ "archive": "Arquivar",
+ "nothing-selected": "Nada selecionado",
+ "add-tag-placeholder": "Aperte enter para adicionar a tag",
+ "no-subject": "<Sem assunto>",
+ "no-recipient": "<Sem destinatários>",
+ "you": "você",
+ "encrypted": "Criptografado",
+ "not-encrypted": "Não criptografado",
+ "signed": "Rementente verificado",
+ "not-signed": "Não assinado",
+ "sending-mail": "Enviando...",
+ "trash-button": "Deletar",
+ "search-placeholder" : "Pesquisar...",
+ "search-results-for": "Resultado da pesquisa por",
+ "forward": "Encaminhar",
+ "feedback-placeholder": "Nos diga o que gosta, não gosta, o que está faltando e o que pensa sobre o Pixelated.",
+ "user-account": "Opções de usuário",
+ "email-address": "Endereço de email",
+ "public-key-fingerprint": "Identificação da chave pública",
+ "version": "versão",
+ "logout": "Sair",
+ "delete-permanently": "Excluir permanentemente",
+ "move-to-inbox": "Mover para Caixa de Entrada",
+ "reply-author-line": "Em {{date}}, <{{from}}> escreveu:\n",
+ "refresh": "atualizar",
+ "click-to-remove": "Pressione para remover",
+ "no-results-for": "Sem resultados para",
+ "no-emails-in": "Nenhum email em",
+ "error": {
+ "timeout": "A operação excedeu o limite de tempo",
+ "general": "Problemas ao se comunicar com o servidor",
+ "parse": "Obteve uma resposta inválida do servidor"
+ },
+ "tags": {
+ "inbox": "Caixa de Entrada",
+ "sent": "Enviadas",
+ "drafts": "Rascunhos",
+ "trash": "Lixeira",
+ "all": "Todas",
+ "tags": "Etiquetas"
+ }
+}
diff --git a/web-ui/app/locales/sv_SE/translation.json b/web-ui/app/locales/sv_SE/translation.json
new file mode 100644
index 00000000..d4da0711
--- /dev/null
+++ b/web-ui/app/locales/sv_SE/translation.json
@@ -0,0 +1,42 @@
+{
+ "compose": "Skriv nytt",
+ "re": "Sv",
+ "fwd": "VB",
+ "trash-single": "Ditt meddelande har flyttats till papperskorgen!",
+ "trash-bulk": "Ditt meddelande har arkiverats!",
+ "recipients-not-valid": "En eller flera mottagare är inte giltiga epost-adresser",
+ "failed-change-tags": "Kan inte ändra taggar",
+ "invalid-tag-name": "Ogiltigt taggnamn",
+ "failed-delete-single": "Kan inte ta bort meddelande",
+ "failed-fetch-messages": "Kan inte hämta meddelanden",
+ "to": "till",
+ "cc": "CC",
+ "bcc": "BCC",
+ "body": "Innehåll",
+ "subject": "Titel",
+ "send": "Skicka",
+ "reply": "Svara",
+ "reply-to-all": "Svara Alla",
+ "mark-as-read": "Markera som läst",
+ "delete": "Ta bort",
+ "archive": "Arkivera",
+ "nothing-selected": "INGET VALT",
+ "add-tag-placeholder": "Tryck retur för att skapa",
+ "no-subject": "<Ingen titel>",
+ "no-recipient": "<Inga mottagare>",
+ "you": "du",
+ "encrypted": "krypterad",
+ "not-encrypted": "Meddelandet var läsbart medans det var på väg.",
+ "signed": "Certifierad avsändare.",
+ "not-signed": "Avsändaren kunde inte säkert identifieras.",
+ "search-placeholder" : "Sök...",
+ "search-results-for": "Sökresultat för",
+ "forward": "Vidarebefodra",
+ "tags": {
+ "inbox": "Inlåda",
+ "sent": "Skickat",
+ "drafts": "Utkast",
+ "trash": "Skräp",
+ "all": "Alla"
+ }
+}
diff --git a/web-ui/app/robots.txt b/web-ui/app/robots.txt
new file mode 100644
index 00000000..6b0157e2
--- /dev/null
+++ b/web-ui/app/robots.txt
@@ -0,0 +1,3 @@
+# robotstxt.org
+
+User-agent: * \ No newline at end of file
diff --git a/web-ui/app/sandbox.html b/web-ui/app/sandbox.html
new file mode 100644
index 00000000..8325b0da
--- /dev/null
+++ b/web-ui/app/sandbox.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <link href="css/sandbox.css" rel="stylesheet" type="text/css">
+
+ <!--usemin_start-->
+ <script src="js/sandbox.js"></script>
+ <script src="bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script>
+ <!--usemin_end-->
+</head>
+
+<body></body>
+
+</html>
diff --git a/web-ui/app/scss/_mixins.scss b/web-ui/app/scss/_mixins.scss
new file mode 100644
index 00000000..d3aa0220
--- /dev/null
+++ b/web-ui/app/scss/_mixins.scss
@@ -0,0 +1,71 @@
+// SHARED MIXINS
+@mixin btn-transition {
+ @include transition-property(background-color);
+ @include transition-duration(300ms);
+ @include transition-timing-function(ease-out);
+}
+
+@mixin tooltip($top: 8px, $left: 40px) {
+ background: rgba(0, 0, 0, 0.7);
+ color: $white;
+ position: absolute;
+ z-index: 2;
+ left: $left;
+ top: $top;
+ font-size: 0.8rem;
+ padding: 2px 10px;
+ white-space: nowrap;
+ @include border-radius(2px);
+}
+
+// FORM MIXINS
+@mixin check-box {
+ background-color: $white;
+ border: 1px solid $light_gray;
+ padding: 7px;
+ margin: 3px 0;
+ cursor: pointer;
+ display: inline-block;
+ position: relative;
+ @include border-radius(2px);
+ @include appearance(none);
+
+ &:focus {
+ outline: none;
+ border-color: $medium_dark_grey;
+ }
+
+ &:active, &:checked:active {
+ }
+
+ &:checked {
+ background-color: $contrast;
+ border: 1px solid darken($lighter_gray, 10%);
+ color: $dark_grey;
+ }
+
+ &:checked:after {
+ content: '\2714';
+ font-size: 1em;
+ position: absolute;
+ bottom: -2px;
+ left: 1px;
+ color: $navigation_background;
+ }
+}
+
+
+@mixin searching($top, $left, $color, $size){
+ &.searching {
+ &:after {
+ font-family: FontAwesome;
+ content: "\f002";
+ font-size: $size;
+ top: $top;
+ left: $left;
+ position: absolute;
+ color: $color;
+ text-shadow: -1px 0 $contrast, 0 1px $contrast, 1px 0 $contrast, 0 -1px $contrast;
+ }
+ }
+}
diff --git a/web-ui/app/scss/_others.scss b/web-ui/app/scss/_others.scss
new file mode 100644
index 00000000..039d94bd
--- /dev/null
+++ b/web-ui/app/scss/_others.scss
@@ -0,0 +1,72 @@
+.hidden {
+ display: none;
+}
+
+.no-padding {
+ padding: 0;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.search-highlight {
+ background-color: $search-highlight;
+}
+
+button {
+ border: 1px solid transparent;
+
+ i {
+ margin-left: 5px;
+ }
+
+ &#trash-button {
+ background: $white;
+ border: 1px solid $medium_light_grey;
+ color: $medium_light_grey;
+ float: right;
+ margin-left: 5px;
+
+ &:hover, &:focus {
+ background: $contrast;
+ }
+ }
+
+ &.no-style {
+ background: transparent;
+ color: $medium_light_grey;
+ padding: 0;
+ margin: 0;
+
+ i {
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+ }
+ }
+}
+
+section {
+ display: inline-block;
+ vertical-align: top;
+ height: 100vh;
+ overflow-y: scroll;
+
+ &#left-pane {
+ background-color: $navigation_background;
+ color: white;
+ }
+
+ &#middle-pane {
+ background: $white;
+ }
+
+ &#right-pane {
+ padding: 0 10px 60px 0px;
+ background: $white;
+ box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.12);
+ z-index: 2;
+ overflow-y: auto;
+ }
+}
diff --git a/web-ui/app/scss/base/_colors.scss b/web-ui/app/scss/base/_colors.scss
new file mode 100644
index 00000000..17333ff9
--- /dev/null
+++ b/web-ui/app/scss/base/_colors.scss
@@ -0,0 +1,64 @@
+/* Pixelated Color Palette - don't change these! */
+$dark_slate_gray: #3E3A37;
+$light_gray: #C2C2C2;
+$lighter_blue: #91C2D1;
+$light_blue: #3DABC4;
+$dark_blue: #178CA6;
+$bullet-blue: #5cacde;
+$light_orange: #FF9C00;
+$dark_orange: #FF7902;
+
+
+/* Side nav background color */
+$navigation_background: $dark_slate_gray;
+
+/* Action buttons and links */
+$action_buttons: $light_blue;
+
+/* Primary Highlight*/
+$primary_highlight: $light_orange;
+
+/* Logo color*/
+$logo_color: $light_orange;
+
+/* Unread count dialog bubble background color */
+$secondary_callout: darken($primary_highlight, 5);
+
+/* Grayscale */
+$contrast: #EEE;
+$white: #FFF;
+$dark_white: #FAFAFA;
+$lighter_gray: #DDD;
+$medium_light_grey: #999;
+$medium_grey: #777;
+$medium_dark_grey: #666;
+$dark_grey: #333;
+$black: #000;
+$top_pane: $contrast;
+$total_count_bg: #C0B9B9;
+$background_dropdown_grey: #f0f0f0;
+
+$background_light_grey: #F5F5F5;
+$border_light_grey: #D9D9D9;
+
+/* Feedback to Users */
+$warning: #F7E8AF;
+$search-highlight: #FFEF29;
+
+/* Light gray indicator icons */
+$indicator_icon_color: $light_gray;
+
+$error: #D93C38;
+$attention: #F6A41C;
+$success: #50BA5B;
+
+$will_be_encrypted: $success;
+$wont_be_encrypted: $attention;
+$recipients_font_color: #828282;
+
+/* Attachments */
+$attachment_text: #555;
+$attachment_icon: lighten($attachment_text, 30);
+$attachment_size: lighten($attachment_text, 30);
+$attachment_area_background: #F5F5F5;
+$attachment_area_border: #D9D9D9;
diff --git a/web-ui/app/scss/base/_fonts.scss b/web-ui/app/scss/base/_fonts.scss
new file mode 100644
index 00000000..dfc56dd8
--- /dev/null
+++ b/web-ui/app/scss/base/_fonts.scss
@@ -0,0 +1,68 @@
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ src: local('Open Sans Light'), local('OpenSans-Light'), url('/assets/fonts/OpenSans-Light.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Open Sans'), local('OpenSans'), url('/assets/fonts/OpenSans.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url('/assets/fonts/OpenSans-Semibold.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Open Sans Bold'), local('OpenSans-Bold'), url('/assets/fonts/OpenSans-Bold.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 800;
+ src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'), url('/assets/fonts/OpenSans-Extrabold.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url('/assets/fonts/OpenSansLight-Italic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ src: local('Open Sans Italic'), local('OpenSans-Italic'), url('/assets/fonts/OpenSans-Italic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ src: local('Open Sans Semibold Italic'), local('OpenSans-SemiboldItalic'), url('/assets/fonts/OpenSans-SemiboldItalic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 700;
+ src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url('/assets/fonts/OpenSans-BoldItalic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 800;
+ src: local('Open Sans Extrabold Italic'), local('OpenSans-ExtraboldItalic'), url('/assets/fonts/OpenSans-ExtraboldItalic.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'icomoon';
+ font-style: normal;
+ font-weight: 400;
+ src: url('/assets/fonts/icomoon.woff') format('woff'), url('/assets/fonts/icomoon.ttf') format('truetype'), ;
+}
+
diff --git a/web-ui/app/scss/base/_scaffolding.scss b/web-ui/app/scss/base/_scaffolding.scss
new file mode 100644
index 00000000..b8b5fa3b
--- /dev/null
+++ b/web-ui/app/scss/base/_scaffolding.scss
@@ -0,0 +1,10 @@
+html {
+ height: 100% ;
+}
+
+body {
+ min-height: 100% ;
+ overflow: hidden;
+ background: $white;
+}
+
diff --git a/web-ui/app/scss/mixins/_position-helpers.scss b/web-ui/app/scss/mixins/_position-helpers.scss
new file mode 100644
index 00000000..254bfc6c
--- /dev/null
+++ b/web-ui/app/scss/mixins/_position-helpers.scss
@@ -0,0 +1,9 @@
+@mixin absolute-center-unknown-height-width() {
+ margin: auto;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ -ms-transform: translate(-50%, -50%);
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+}
diff --git a/web-ui/app/scss/mixins/_tags.scss b/web-ui/app/scss/mixins/_tags.scss
new file mode 100644
index 00000000..9bb287ea
--- /dev/null
+++ b/web-ui/app/scss/mixins/_tags.scss
@@ -0,0 +1,110 @@
+$tags-font-size: 0.6rem;
+
+@mixin tags {
+ & > * {
+ display: inline;
+ }
+
+ &-tag {
+ font-size: $tags-font-size;
+ font-weight: 700;
+ background-color: $dark_blue;
+ color: white;
+ padding: 2px 4px;
+ margin: 0 1px;
+ border-radius: 2px;
+ }
+}
+
+@mixin tags-editable {
+ @include tags;
+
+ &-tag:hover {
+ text-decoration: line-through;
+ cursor: pointer;
+ position: relative;
+
+ &:before {
+ @include tooltip(130%, 25%);
+
+ content: "click to remove";
+ text-transform: lowercase;
+ }
+ }
+
+ &-label {
+ vertical-align: bottom;
+ color: $light_gray;
+ }
+
+ &-new-button {
+ font-size: $tags-font-size;
+ padding: 0;
+ background: transparent;
+ border-radius: 2px;
+ padding: 2px;
+
+ &:hover {
+ opacity: 1;
+ background: $lighter_gray;
+ }
+ }
+
+ &-name-input {
+ opacity: 0.6;
+ transition: background-color 150ms ease-out;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ // twitter typeahead classes. those are set via JS, with relatively high specificity,
+ // hence box-model-related properties are repeated
+ // https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#class-names
+
+ $suggestion-border: 1px solid darken($contrast, 5%);
+ $input-field-padding: 1px 5px;
+ $input-field-margin: 2px;
+
+ & * .tt-input {
+ border-radius: $input-field-margin;
+ padding: $input-field-padding;
+ margin-top: 2px;
+ font-size: $tags-font-size;
+ }
+
+ & * .tt-hint {
+ color: $medium_light_grey;
+ padding: $input-field-padding;
+ margin-top: $input-field-margin;
+ font-size: $tags-font-size;
+ background: transparent;
+ }
+
+ & * .tt-dropdown-menu {
+ min-width: 250px;
+ padding: 0;
+ font-size: $tags-font-size;
+ background-color: $contrast;
+ border: $suggestion-border;
+ }
+
+ & * .tt-suggestion {
+ padding: 5px 10px;
+ font-size: $tags-font-size;
+ border-bottom: $suggestion-border;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ p {
+ margin: 0;
+ }
+ }
+
+ & * .tt-cursor {
+ background-color: $white;
+ }
+ }
+}
diff --git a/web-ui/app/scss/sandbox.scss b/web-ui/app/scss/sandbox.scss
new file mode 100644
index 00000000..3c1be358
--- /dev/null
+++ b/web-ui/app/scss/sandbox.scss
@@ -0,0 +1,27 @@
+$search-highlight: #FFEF29;
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Open Sans'), local('OpenSans'), url('/sandbox/fonts/OpenSans.woff') format('woff');
+}
+
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ font-size: 13px;
+ line-height: 1.2em;
+ background: white;
+ color: #333;
+ padding: 0;
+ margin: 0;
+ font-weight: normal;
+ -webkit-font-smoothing: antialiased;
+ font-style: normal;
+ box-sizing: border-box;
+ word-wrap: break-word;
+}
+
+.search-highlight {
+ background-color: $search-highlight;
+}
diff --git a/web-ui/app/scss/style.scss b/web-ui/app/scss/style.scss
new file mode 100644
index 00000000..e99ab194
--- /dev/null
+++ b/web-ui/app/scss/style.scss
@@ -0,0 +1,39 @@
+// vendor stylesheets and resets
+@import "vendor/reset";
+@import "vendor/scut";
+@import "compass/css3";
+@import "vendor/foundation";
+@import "vendor/customfont";
+
+// basic configuration
+@import "base/fonts";
+@import "base/colors";
+@import "base/scaffolding";
+
+// mixins
+@import "mixins/position-helpers";
+@import "mixins/tags";
+
+// TODO
+@import "mixins";
+
+// templates
+@import "templates/no-content-placeholder";
+@import "templates/unread-count";
+
+// views
+@import "views/message-panel";
+@import "views/close-button";
+@import "views/no-message-selected";
+@import "views/no-mails-available";
+@import "views/read-view";
+@import "views/security-labels";
+@import "views/compose-view";
+@import "views/compose-button";
+@import "views/mail-list";
+@import "views/_action-bar.scss";
+@import "views/_navigation.scss";
+
+// misc stuff
+@import "others";
+
diff --git a/web-ui/app/scss/templates/_no-content-placeholder.scss b/web-ui/app/scss/templates/_no-content-placeholder.scss
new file mode 100644
index 00000000..c6807011
--- /dev/null
+++ b/web-ui/app/scss/templates/_no-content-placeholder.scss
@@ -0,0 +1,5 @@
+.no-content-placeholder {
+ @include absolute-center-unknown-height-width;
+
+ color: $medium_dark_grey;
+}
diff --git a/web-ui/app/scss/templates/_unread-count.scss b/web-ui/app/scss/templates/_unread-count.scss
new file mode 100644
index 00000000..f7852227
--- /dev/null
+++ b/web-ui/app/scss/templates/_unread-count.scss
@@ -0,0 +1,14 @@
+.mail-count {
+ background: $white;
+ border-radius: 50%;
+ border: 1px solid $white;
+ color: $white;
+ font-size: 0.7em;
+ font-weight: 700;
+ left: 0;
+ margin-left: 5px;
+ opacity: 0.95;
+ padding: 0px 5px 0;
+ position: absolute;
+ top: 1px;
+}
diff --git a/web-ui/app/scss/vendor/_customfont.scss b/web-ui/app/scss/vendor/_customfont.scss
new file mode 100644
index 00000000..d72cca0f
--- /dev/null
+++ b/web-ui/app/scss/vendor/_customfont.scss
@@ -0,0 +1,9 @@
+[class^="icon-"], [class*=" icon-"] {
+ /* use !important to prevent issues with browser extensions that change fonts */
+ font-family: 'icomoon' !important;
+ line-height: 1;
+}
+
+.icon-px-sent:before {
+ content: "\e900";
+}
diff --git a/web-ui/app/scss/vendor/_foundation.scss b/web-ui/app/scss/vendor/_foundation.scss
new file mode 100644
index 00000000..7918cf26
--- /dev/null
+++ b/web-ui/app/scss/vendor/_foundation.scss
@@ -0,0 +1,2066 @@
+@import 'compass/css3';
+
+meta {
+ &.foundation-version {
+ font-family: "/5.2.3/";
+ }
+ &.foundation-mq-small {
+ font-family: "/only screen/";
+ width: 0em;
+ }
+ &.foundation-mq-medium {
+ font-family: "/only screen and (min-width:40.063em)/";
+ width: 40.063em;
+ }
+ &.foundation-mq-large {
+ font-family: "/only screen and (min-width:64.063em)/";
+ width: 64.063em;
+ }
+ &.foundation-mq-xlarge {
+ font-family: "/only screen and (min-width:90.063em)/";
+ width: 90.063em;
+ }
+ &.foundation-mq-xxlarge {
+ font-family: "/only screen and (min-width:120.063em)/";
+ width: 120.063em;
+ }
+ &.foundation-data-attribute-namespace {
+ font-family: false;
+ }
+}
+
+html, body {
+ height: 100%;
+}
+
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ &:before, &:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+}
+
+html {
+ font-size: 100%;
+}
+
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ font-size: 13px;
+ line-height: 1.2em;
+ background: white;
+ color: #333;
+ padding: 0;
+ margin: 0;
+ font-weight: normal;
+ -webkit-font-smoothing: antialiased;
+ font-style: normal;
+ position: relative;
+ cursor: default;
+}
+
+a:hover {
+ cursor: pointer;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+ -ms-interpolation-mode: bicubic;
+}
+
+#map_canvas {
+ img, embed, object {
+ max-width: none !important;
+ }
+}
+
+.map_canvas {
+ img, embed, object {
+ max-width: none !important;
+ }
+}
+
+.left {
+ float: left !important;
+}
+
+.right {
+ float: right !important;
+}
+
+.clearfix {
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+}
+
+.hide {
+ display: none;
+}
+
+.antialiased {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+img {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+textarea {
+ height: auto;
+ min-height: 50px;
+ &:focus {
+ outline: none;
+ }
+}
+
+select {
+ width: 100%;
+}
+
+.row {
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ margin-top: 0;
+ margin-bottom: 0;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+ &.collapse {
+ > {
+ .column, .columns {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
+ .row {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+ .row {
+ width: auto;
+ margin-left: -0.9375em;
+ margin-right: -0.9375em;
+ margin-top: 0;
+ margin-bottom: 0;
+ max-width: none;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+ &.collapse {
+ width: auto;
+ margin: 0;
+ max-width: none;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+ }
+ }
+}
+
+.column, .columns {
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ width: 100%;
+ float: left;
+}
+
+@media only screen {
+ .small-push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .small-pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .small-push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .small-pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .small-push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .small-pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .small-push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .small-pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .small-push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .small-pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .small-push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .small-pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .small-push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .small-pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .small-push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .small-pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .small-push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .small-pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .small-push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .small-pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .small-push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .small-pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .small-push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .small-pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+ .column, .columns {
+ position: relative;
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ float: left;
+ }
+ .small-1 {
+ width: 8.33333%;
+ }
+ .small-2 {
+ width: 16.66667%;
+ }
+ .small-3 {
+ width: 25%;
+ }
+ .small-4 {
+ width: 33.33333%;
+ }
+ .small-5 {
+ width: 41.66667%;
+ }
+ .small-6 {
+ width: 50%;
+ }
+ .small-7 {
+ width: 58.33333%;
+ }
+ .small-8 {
+ width: 66.66667%;
+ }
+ .small-9 {
+ width: 75%;
+ }
+ .small-10 {
+ width: 83.33333%;
+ }
+ .small-11 {
+ width: 91.66667%;
+ }
+ .small-12 {
+ width: 100%;
+ }
+ [class*="column"] + [class*="column"] {
+ &:last-child {
+ float: right;
+ }
+ &.end {
+ float: left;
+ }
+ }
+ .small-offset-0 {
+ margin-left: 0% !important;
+ }
+ .small-offset-1 {
+ margin-left: 8.33333% !important;
+ }
+ .small-offset-2 {
+ margin-left: 16.66667% !important;
+ }
+ .small-offset-3 {
+ margin-left: 25% !important;
+ }
+ .small-offset-4 {
+ margin-left: 33.33333% !important;
+ }
+ .small-offset-5 {
+ margin-left: 41.66667% !important;
+ }
+ .small-offset-6 {
+ margin-left: 50% !important;
+ }
+ .small-offset-7 {
+ margin-left: 58.33333% !important;
+ }
+ .small-offset-8 {
+ margin-left: 66.66667% !important;
+ }
+ .small-offset-9 {
+ margin-left: 75% !important;
+ }
+ .small-offset-10 {
+ margin-left: 83.33333% !important;
+ }
+ .small-offset-11 {
+ margin-left: 91.66667% !important;
+ }
+ .small-reset-order {
+ margin-left: 0;
+ margin-right: 0;
+ left: auto;
+ right: auto;
+ float: left;
+ }
+ .column.small-centered, .columns.small-centered {
+ margin-left: auto;
+ margin-right: auto;
+ float: none !important;
+ }
+ .column.small-uncentered, .columns.small-uncentered {
+ margin-left: 0;
+ margin-right: 0;
+ float: left !important;
+ }
+ .column.small-uncentered.opposite, .columns.small-uncentered.opposite {
+ float: right;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ .medium-push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .medium-pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .medium-push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .medium-pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .medium-push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .medium-pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .medium-push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .medium-pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .medium-push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .medium-pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .medium-push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .medium-pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .medium-push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .medium-pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .medium-push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .medium-pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .medium-push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .medium-pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .medium-push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .medium-pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .medium-push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .medium-pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .medium-push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .medium-pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+ .column, .columns {
+ position: relative;
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ float: left;
+ }
+ .medium-1 {
+ width: 8.33333%;
+ }
+ .medium-2 {
+ width: 16.66667%;
+ }
+ .medium-3 {
+ width: 25%;
+ }
+ .medium-4 {
+ width: 33.33333%;
+ }
+ .medium-5 {
+ width: 41.66667%;
+ }
+ .medium-6 {
+ width: 50%;
+ }
+ .medium-7 {
+ width: 58.33333%;
+ }
+ .medium-8 {
+ width: 66.66667%;
+ }
+ .medium-9 {
+ width: 75%;
+ }
+ .medium-10 {
+ width: 83.33333%;
+ }
+ .medium-11 {
+ width: 91.66667%;
+ }
+ .medium-12 {
+ width: 100%;
+ }
+ [class*="column"] + [class*="column"] {
+ &:last-child {
+ float: right;
+ }
+ &.end {
+ float: left;
+ }
+ }
+ .medium-offset-0 {
+ margin-left: 0% !important;
+ }
+ .medium-offset-1 {
+ margin-left: 8.33333% !important;
+ }
+ .medium-offset-2 {
+ margin-left: 16.66667% !important;
+ }
+ .medium-offset-3 {
+ margin-left: 25% !important;
+ }
+ .medium-offset-4 {
+ margin-left: 33.33333% !important;
+ }
+ .medium-offset-5 {
+ margin-left: 41.66667% !important;
+ }
+ .medium-offset-6 {
+ margin-left: 50% !important;
+ }
+ .medium-offset-7 {
+ margin-left: 58.33333% !important;
+ }
+ .medium-offset-8 {
+ margin-left: 66.66667% !important;
+ }
+ .medium-offset-9 {
+ margin-left: 75% !important;
+ }
+ .medium-offset-10 {
+ margin-left: 83.33333% !important;
+ }
+ .medium-offset-11 {
+ margin-left: 91.66667% !important;
+ }
+ .medium-reset-order {
+ margin-left: 0;
+ margin-right: 0;
+ left: auto;
+ right: auto;
+ float: left;
+ }
+ .column.medium-centered, .columns.medium-centered {
+ margin-left: auto;
+ margin-right: auto;
+ float: none !important;
+ }
+ .column.medium-uncentered, .columns.medium-uncentered {
+ margin-left: 0;
+ margin-right: 0;
+ float: left !important;
+ }
+ .column.medium-uncentered.opposite, .columns.medium-uncentered.opposite {
+ float: right;
+ }
+ .push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+}
+
+@media only screen and (min-width: 64.063em) {
+ .large-push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .large-pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .large-push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .large-pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .large-push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .large-pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .large-push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .large-pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .large-push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .large-pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .large-push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .large-pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .large-push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .large-pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .large-push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .large-pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .large-push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .large-pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .large-push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .large-pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .large-push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .large-pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .large-push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .large-pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+ .column, .columns {
+ position: relative;
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ float: left;
+ }
+ .large-1 {
+ width: 8.33333%;
+ }
+ .large-2 {
+ width: 16.66667%;
+ }
+ .large-3 {
+ width: 25%;
+ }
+ .large-4 {
+ width: 33.33333%;
+ }
+ .large-5 {
+ width: 41.66667%;
+ }
+ .large-6 {
+ width: 50%;
+ }
+ .large-7 {
+ width: 58.33333%;
+ }
+ .large-8 {
+ width: 66.66667%;
+ }
+ .large-9 {
+ width: 75%;
+ }
+ .large-10 {
+ width: 83.33333%;
+ }
+ .large-11 {
+ width: 91.66667%;
+ }
+ .large-12 {
+ width: 100%;
+ }
+ [class*="column"] + [class*="column"] {
+ &:last-child {
+ float: right;
+ }
+ &.end {
+ float: left;
+ }
+ }
+ .large-offset-0 {
+ margin-left: 0% !important;
+ }
+ .large-offset-1 {
+ margin-left: 8.33333% !important;
+ }
+ .large-offset-2 {
+ margin-left: 16.66667% !important;
+ }
+ .large-offset-3 {
+ margin-left: 25% !important;
+ }
+ .large-offset-4 {
+ margin-left: 33.33333% !important;
+ }
+ .large-offset-5 {
+ margin-left: 41.66667% !important;
+ }
+ .large-offset-6 {
+ margin-left: 50% !important;
+ }
+ .large-offset-7 {
+ margin-left: 58.33333% !important;
+ }
+ .large-offset-8 {
+ margin-left: 66.66667% !important;
+ }
+ .large-offset-9 {
+ margin-left: 75% !important;
+ }
+ .large-offset-10 {
+ margin-left: 83.33333% !important;
+ }
+ .large-offset-11 {
+ margin-left: 91.66667% !important;
+ }
+ .large-reset-order {
+ margin-left: 0;
+ margin-right: 0;
+ left: auto;
+ right: auto;
+ float: left;
+ }
+ .column.large-centered, .columns.large-centered {
+ margin-left: auto;
+ margin-right: auto;
+ float: none !important;
+ }
+ .column.large-uncentered, .columns.large-uncentered {
+ margin-left: 0;
+ margin-right: 0;
+ float: left !important;
+ }
+ .column.large-uncentered.opposite, .columns.large-uncentered.opposite {
+ float: right;
+ }
+ .push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+}
+
+.inline-list {
+ margin: 0 auto 1.0625rem auto;
+ margin-left: -1.375rem;
+ margin-right: 0;
+ padding: 0;
+ list-style: none;
+ overflow: hidden;
+ > li {
+ list-style: none;
+ float: left;
+ margin-left: 1.375rem;
+ display: block;
+ > * {
+ display: block;
+ }
+ }
+}
+
+.text-left {
+ text-align: left !important;
+}
+
+.text-right {
+ text-align: right !important;
+}
+
+.text-center {
+ text-align: center !important;
+}
+
+.text-justify {
+ text-align: justify !important;
+}
+
+@media only screen and (max-width: 40em) {
+ .small-only-text-left {
+ text-align: left !important;
+ }
+ .small-only-text-right {
+ text-align: right !important;
+ }
+ .small-only-text-center {
+ text-align: center !important;
+ }
+ .small-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen {
+ .small-text-left {
+ text-align: left !important;
+ }
+ .small-text-right {
+ text-align: right !important;
+ }
+ .small-text-center {
+ text-align: center !important;
+ }
+ .small-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (max-width: 64em) {
+ .medium-only-text-left {
+ text-align: left !important;
+ }
+ .medium-only-text-right {
+ text-align: right !important;
+ }
+ .medium-only-text-center {
+ text-align: center !important;
+ }
+ .medium-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ .medium-text-left {
+ text-align: left !important;
+ }
+ .medium-text-right {
+ text-align: right !important;
+ }
+ .medium-text-center {
+ text-align: center !important;
+ }
+ .medium-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 64.063em) and (max-width: 90em) {
+ .large-only-text-left {
+ text-align: left !important;
+ }
+ .large-only-text-right {
+ text-align: right !important;
+ }
+ .large-only-text-center {
+ text-align: center !important;
+ }
+ .large-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 64.063em) {
+ .large-text-left {
+ text-align: left !important;
+ }
+ .large-text-right {
+ text-align: right !important;
+ }
+ .large-text-center {
+ text-align: center !important;
+ }
+ .large-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 90.063em) and (max-width: 120em) {
+ .xlarge-only-text-left {
+ text-align: left !important;
+ }
+ .xlarge-only-text-right {
+ text-align: right !important;
+ }
+ .xlarge-only-text-center {
+ text-align: center !important;
+ }
+ .xlarge-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 90.063em) {
+ .xlarge-text-left {
+ text-align: left !important;
+ }
+ .xlarge-text-right {
+ text-align: right !important;
+ }
+ .xlarge-text-center {
+ text-align: center !important;
+ }
+ .xlarge-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 120.063em) and (max-width: 99999999em) {
+ .xxlarge-only-text-left {
+ text-align: left !important;
+ }
+ .xxlarge-only-text-right {
+ text-align: right !important;
+ }
+ .xxlarge-only-text-center {
+ text-align: center !important;
+ }
+ .xxlarge-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 120.063em) {
+ .xxlarge-text-left {
+ text-align: left !important;
+ }
+ .xxlarge-text-right {
+ text-align: right !important;
+ }
+ .xxlarge-text-center {
+ text-align: center !important;
+ }
+ .xxlarge-text-justify {
+ text-align: justify !important;
+ }
+}
+
+/* Typography resets */
+
+div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, p, blockquote, th, td {
+ margin: 0;
+ padding: 0;
+}
+
+/* Default Link Styles */
+
+a {
+ color: #2ba6cb;
+ text-decoration: none;
+ line-height: inherit;
+ &:hover, &:focus {
+ color: #258faf;
+ outline: none;
+ }
+ img {
+ border: none;
+ }
+}
+
+/* Default paragraph styles */
+
+p {
+ font-family: inherit;
+ font-weight: normal;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ margin-bottom: 1.25rem;
+ text-rendering: optimizeLegibility;
+ &.lead {
+ font-size: 1.21875rem;
+ line-height: 1.4;
+ }
+ aside {
+ font-size: 0.875rem;
+ line-height: 1.35;
+ font-style: italic;
+ }
+}
+
+/* Default header styles */
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: normal;
+ font-style: normal;
+ color: #222;
+ text-rendering: optimizeLegibility;
+ margin-top: 0.2rem;
+ margin-bottom: 0.5rem;
+ line-height: 1.2;
+}
+
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small {
+ font-size: 60%;
+ color: #6f6f6f;
+ line-height: 0;
+}
+
+h1 {
+ font-size: 2.125rem;
+}
+
+h2 {
+ font-size: 1.6875rem;
+}
+
+h3 {
+ font-size: 1.375rem;
+}
+
+h4, h5 {
+ font-size: 1.125rem;
+}
+
+h6 {
+ font-size: 1rem;
+}
+
+.subheader {
+ line-height: 1.4;
+ color: #6f6f6f;
+ font-weight: normal;
+ margin-top: 0.2rem;
+ margin-bottom: 0.5rem;
+}
+
+hr {
+ border: solid #dddddd;
+ border-width: 1px 0 0;
+ clear: both;
+ margin: 1.25rem 0 1.1875rem;
+ height: 0;
+}
+
+/* Helpful Typography Defaults */
+
+em, i {
+ font-style: italic;
+ line-height: inherit;
+}
+
+strong, b {
+ font-weight: bold;
+ line-height: inherit;
+}
+
+small {
+ font-size: 60%;
+ line-height: inherit;
+}
+
+code {
+ font-family: Consolas, "Liberation Mono", Courier, monospace;
+ font-weight: bold;
+ color: #910b0e;
+}
+
+/* Lists */
+
+ul, ol, dl {
+ font-size: 0.9rem;
+ line-height: 1.6;
+ margin-bottom: 1.25rem;
+ list-style-position: outside;
+ font-family: inherit;
+}
+
+ul {
+ margin-left: 0;
+ &.bullets {
+ margin-left: 1.1rem;
+ li {
+ margin-left: 1.25rem;
+ margin-bottom: 0;
+ list-style: circle;
+ }
+ }
+ li {
+ margin-bottom: 0;
+ list-style: none;
+ }
+}
+
+/* Abbreviations */
+
+abbr, acronym {
+ text-transform: uppercase;
+ font-size: 90%;
+ color: #222222;
+ border-bottom: 1px dotted #dddddd;
+ cursor: help;
+}
+
+abbr {
+ text-transform: none;
+}
+
+/* Blockquotes */
+
+blockquote {
+ margin: 0 0 1.25rem;
+ padding: 0.5625rem 1.25rem 0 1.1875rem;
+ border-left: 1px solid #dddddd;
+ cite {
+ display: block;
+ font-size: 0.8125rem;
+ color: #555555;
+ &:before {
+ content: "\2014 \0020";
+ }
+ a {
+ color: #555555;
+ &:visited {
+ color: #555555;
+ }
+ }
+ }
+ line-height: 1.6;
+ color: #6f6f6f;
+ p {
+ line-height: 1.6;
+ color: #6f6f6f;
+ }
+}
+
+/* Microformats */
+
+.vcard {
+ display: inline-block;
+ margin: 0 0 1.25rem 0;
+ border: 1px solid #dddddd;
+ padding: 0.625rem 0.75rem;
+ li {
+ margin: 0;
+ display: block;
+ }
+ .fn {
+ font-weight: bold;
+ font-size: 0.9375rem;
+ }
+}
+
+.vevent {
+ .summary {
+ font-weight: bold;
+ }
+ abbr {
+ cursor: default;
+ text-decoration: none;
+ font-weight: bold;
+ border: none;
+ padding: 0 0.0625rem;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ h1, h2, h3, h4, h5, h6 {
+ line-height: 1.2;
+ }
+ h1 {
+ font-size: 2.55rem;
+ }
+ h2 {
+ font-size: 2.3125rem;
+ }
+ h3 {
+ font-size: 1.4875rem;
+ }
+ h4 {
+ font-size: 1.1375rem;
+ }
+}
+
+/*
+ * Print styles.
+ *
+ * Inlined to avoid required HTTP connection: www.phpied.com/delay-loading-your-print-css/
+ * Credit to Paul Irish and HTML5 Boilerplate (html5boilerplate.com)
+*/
+
+.print-only {
+ display: none !important;
+}
+
+@media print {
+ * {
+ background: transparent !important;
+ color: black !important;
+ /* Black prints faster: h5bp.com/s */
+ box-shadow: none !important;
+ text-shadow: none !important;
+ }
+ a {
+ text-decoration: underline;
+ &:visited {
+ text-decoration: underline;
+ }
+ &[href]:after {
+ content: " (" attr(href) ")";
+ }
+ }
+ abbr[title]:after {
+ content: " (" attr(title) ")";
+ }
+ .ir a:after {
+ content: "";
+ }
+ a {
+ &[href^="javascript:"]:after, &[href^="#"]:after {
+ content: "";
+ }
+ }
+ pre, blockquote {
+ border: 1px solid #999999;
+ page-break-inside: avoid;
+ }
+ thead {
+ display: table-header-group;
+ /* h5bp.com/t */
+ }
+ tr {
+ page-break-inside: avoid;
+ }
+ img {
+ page-break-inside: avoid;
+ max-width: 100% !important;
+ }
+ @page {
+ margin: 0.5cm;
+ }
+
+ p, h2, h3 {
+ orphans: 3;
+ widows: 3;
+ }
+ h2, h3 {
+ page-break-after: avoid;
+ }
+ .hide-on-print {
+ display: none !important;
+ }
+ .print-only {
+ display: block !important;
+ }
+ .hide-for-print {
+ display: none !important;
+ }
+ .show-for-print {
+ display: inherit !important;
+ }
+}
+
+.reveal-modal-bg {
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ background: black;
+ background: rgba(0, 0, 0, 0.45);
+ z-index: 99;
+ display: none;
+ top: 0;
+ left: 0;
+}
+
+dialog, .reveal-modal {
+ visibility: hidden;
+ display: none;
+ position: absolute;
+ z-index: 100;
+ width: 100vw;
+ top: 0;
+ left: 0;
+ background-color: white;
+ padding: 1.25rem;
+ border: solid 1px #666666;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
+}
+
+@media only screen and (max-width: 40em) {
+ dialog, .reveal-modal {
+ min-height: 100vh;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ left: 50%;
+ }
+}
+
+dialog {
+ .column, .columns {
+ min-width: 0;
+ }
+}
+
+.reveal-modal {
+ .column, .columns {
+ min-width: 0;
+ }
+}
+
+dialog > :first-child, .reveal-modal > :first-child {
+ margin-top: 0;
+}
+
+dialog > :last-child, .reveal-modal > :last-child {
+ margin-bottom: 0;
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ margin-left: -26%;
+ width: 50%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ top: 6.25rem;
+ }
+}
+
+dialog .close-reveal-modal, .reveal-modal .close-reveal-modal {
+ font-size: 2.5rem;
+ line-height: 1;
+ position: absolute;
+ top: 0.5rem;
+ right: 0.6875rem;
+ color: #aaaaaa;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+dialog[open] {
+ display: block;
+ visibility: visible;
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ padding: 1.875rem;
+ }
+ dialog.radius, .reveal-modal.radius {
+ border-radius: 3px;
+ }
+ dialog.round, .reveal-modal.round {
+ border-radius: 1000px;
+ }
+ dialog.collapse, .reveal-modal.collapse {
+ padding: 0;
+ }
+ dialog.full, .reveal-modal.full {
+ top: 0;
+ left: 0;
+ height: 100vh;
+ min-height: 100vh;
+ margin-left: 0 !important;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.tiny, .reveal-modal.tiny {
+ margin-left: -15%;
+ width: 30%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.small, .reveal-modal.small {
+ margin-left: -20%;
+ width: 40%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.medium, .reveal-modal.medium {
+ margin-left: -30%;
+ width: 60%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.large, .reveal-modal.large {
+ margin-left: -35%;
+ width: 70%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.xlarge, .reveal-modal.xlarge {
+ margin-left: -47.5%;
+ width: 95%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.full, .reveal-modal.full {
+ margin-left: -50vw;
+ width: 100vw;
+ }
+}
+
+@media print {
+ dialog, .reveal-modal {
+ background: white !important;
+ }
+}
+
+.label {
+ font-weight: normal;
+ text-align: center;
+ text-decoration: none;
+ line-height: 1;
+ white-space: nowrap;
+ display: inline-block;
+ position: relative;
+ margin-bottom: inherit;
+ padding: 0.25rem 0.5rem 0.375rem;
+ font-size: 0.6875rem;
+ background-color: #2ba6cb;
+ color: white;
+ &.radius {
+ border-radius: 3px;
+ }
+ &.round {
+ border-radius: 1000px;
+ }
+ &.alert {
+ background-color: #c60f13;
+ color: white;
+ }
+ &.success {
+ background-color: #5da423;
+ color: white;
+ }
+ &.secondary {
+ background-color: #e9e9e9;
+ color: #333333;
+ }
+}
+
+button, .button, input[type=button] {
+ cursor: pointer;
+ margin: 0 0 1.25rem;
+ border: none;
+ position: relative;
+ text-decoration: none;
+ text-align: center;
+ -webkit-appearance: none;
+ display: inline-block;
+ padding: 0.4rem 1.1rem;
+ font-size: 0.9rem;
+ background-color: #2ba6cb;
+ border-color: #2285a2;
+ color: white;
+ transition: background-color 150ms ease-out;
+ @include border-radius(2px);
+ &:hover, &:focus {
+ background-color: #2285a2;
+ outline: none;
+ color: white;
+ }
+ &.large {
+ padding-top: 1.125rem;
+ padding-right: 2.25rem;
+ padding-bottom: 1.1875rem;
+ padding-left: 2.25rem;
+ font-size: 1.25rem;
+ }
+
+ &.small {
+ padding-top: 0.875rem;
+ padding-right: 1.75rem;
+ padding-bottom: 0.9375rem;
+ padding-left: 1.75rem;
+ font-size: 0.8125rem;
+ }
+
+ &.tiny {
+ padding-top: 0.625rem;
+ padding-right: 1.25rem;
+ padding-bottom: 0.6875rem;
+ padding-left: 1.25rem;
+ font-size: 0.6875rem;
+ }
+
+ &.expand {
+ padding-right: 0;
+ padding-left: 0;
+ width: 100%;
+ }
+
+ &.left-align {
+ text-align: left;
+ text-indent: 0.75rem;
+ }
+
+ &.right-align {
+ text-align: right;
+ padding-right: 0.75rem;
+ }
+
+ &.round {
+ border-radius: 1000px;
+ }
+
+ &.disabled, &[disabled] {
+ background-color: #2285a2;
+ border-color: #2285a2;
+ color: white;
+ cursor: default;
+ opacity: 0.5;
+ box-shadow: none;
+ &:hover, &:focus {
+ background-color: #2285a2;
+ opacity: 0.5;
+ }
+ }
+}
+
+
+@media only screen and (min-width: 40.063em) {
+ button, .button {
+ display: inline-block;
+ }
+}
+
+.keystroke, kbd {
+ background-color: #ededed;
+ border-color: #dddddd;
+ color: #222222;
+ border-style: solid;
+ border-width: 1px;
+ margin: 0;
+ font-family: "Consolas", "Menlo", "Courier", monospace;
+ font-size: inherit;
+ padding: 0.125rem 0.25rem 0;
+ border-radius: 3px;
+}
+
+
+
+/* We use this to get basic styling on all basic form elements */
+input[type="text"],
+input[type="password"],
+input[type="date"],
+input[type="datetime"],
+input[type="datetime-local"],
+input[type="month"],
+input[type="week"],
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="tel"],
+input[type="time"],
+input[type="url"],
+textarea {
+ -webkit-appearance: none;
+ background-color: white;
+ font-family: inherit;
+ border: 1px solid #cccccc;
+ color: rgba(0, 0, 0, 0.75);
+ display: block;
+ font-size: 0.875rem;
+ margin: 0 0 1rem 0;
+ padding: 0.4rem;
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+ input[type="text"]:focus,
+ input[type="password"]:focus,
+ input[type="date"]:focus,
+ input[type="datetime"]:focus,
+ input[type="datetime-local"]:focus,
+ input[type="month"]:focus,
+ input[type="week"]:focus,
+ input[type="email"]:focus,
+ input[type="number"]:focus,
+ input[type="search"]:focus,
+ input[type="tel"]:focus,
+ input[type="time"]:focus,
+ input[type="url"]:focus,
+ textarea:focus {}
+ input[type="text"]:focus,
+ input[type="password"]:focus,
+ input[type="date"]:focus,
+ input[type="datetime"]:focus,
+ input[type="datetime-local"]:focus,
+ input[type="month"]:focus,
+ input[type="week"]:focus,
+ input[type="email"]:focus,
+ input[type="number"]:focus,
+ input[type="search"]:focus,
+ input[type="tel"]:focus,
+ input[type="time"]:focus,
+ input[type="url"]:focus,
+ textarea:focus {
+ background: #fafafa;
+ border-color: #999999;
+ outline: none; }
+ input[type="text"][disabled], fieldset[disabled] input[type="text"],
+ input[type="password"][disabled], fieldset[disabled]
+ input[type="password"],
+ input[type="date"][disabled], fieldset[disabled]
+ input[type="date"],
+ input[type="datetime"][disabled], fieldset[disabled]
+ input[type="datetime"],
+ input[type="datetime-local"][disabled], fieldset[disabled]
+ input[type="datetime-local"],
+ input[type="month"][disabled], fieldset[disabled]
+ input[type="month"],
+ input[type="week"][disabled], fieldset[disabled]
+ input[type="week"],
+ input[type="email"][disabled], fieldset[disabled]
+ input[type="email"],
+ input[type="number"][disabled], fieldset[disabled]
+ input[type="number"],
+ input[type="search"][disabled], fieldset[disabled]
+ input[type="search"],
+ input[type="tel"][disabled], fieldset[disabled]
+ input[type="tel"],
+ input[type="time"][disabled], fieldset[disabled]
+ input[type="time"],
+ input[type="url"][disabled], fieldset[disabled]
+ input[type="url"],
+ textarea[disabled], fieldset[disabled]
+ textarea {
+ background-color: #dddddd; }
+ input[type="text"].radius,
+ input[type="password"].radius,
+ input[type="date"].radius,
+ input[type="datetime"].radius,
+ input[type="datetime-local"].radius,
+ input[type="month"].radius,
+ input[type="week"].radius,
+ input[type="email"].radius,
+ input[type="number"].radius,
+ input[type="search"].radius,
+ input[type="tel"].radius,
+ input[type="time"].radius,
+ input[type="url"].radius,
+ textarea.radius {
+ border-radius: 3px; }
+
+input[type="submit"] {
+ -webkit-appearance: none; }
+
+/* Respect enforced amount of rows for textarea */
+textarea[rows] {
+ height: auto; }
+
+/* Add height value for select elements to match text input height */
+select {
+ -webkit-appearance: none !important;
+ background-color: #fafafa;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iM3B4IiB2aWV3Qm94PSIwIDAgNiAzIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA2IDMiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwb2x5Z29uIHBvaW50cz0iNS45OTIsMCAyLjk5MiwzIC0wLjAwOCwwICIvPjwvc3ZnPg==");
+ background-repeat: no-repeat;
+ background-position: 97% center;
+ border: 1px solid #cccccc;
+ padding: 0.5rem;
+ font-size: 0.875rem;
+ color: rgba(0, 0, 0, 0.75);
+ line-height: normal;
+ border-radius: 0;
+}
+ select.radius {
+ border-radius: 3px; }
+ select:hover {
+ background-color: #f3f3f3;
+ border-color: #999999; }
+
+/* Adjust margin for form elements below */
+input[type="file"],
+input[type="checkbox"],
+input[type="radio"],
+select {
+ margin: 0 0 1rem 0; }
+
+input[type="checkbox"] + label,
+input[type="radio"] + label {
+ display: inline-block;
+
+ margin-left: 0.5rem;
+ margin-right: 1rem;
+ margin-bottom: 0;
+ vertical-align: baseline; }
+
+/* Normalize file input width */
+input[type="file"] {
+ width: 100%; }
diff --git a/web-ui/app/scss/vendor/_reset.scss b/web-ui/app/scss/vendor/_reset.scss
new file mode 100644
index 00000000..55f8d054
--- /dev/null
+++ b/web-ui/app/scss/vendor/_reset.scss
@@ -0,0 +1,421 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif;
+ /* 1 */
+ -ms-text-size-adjust: 100%;
+ /* 2 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio, canvas, progress, video {
+ display: inline-block;
+ /* 1 */
+ vertical-align: baseline;
+ /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden], template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background: transparent;
+ &:active, &:hover {
+ outline: 0;
+ }
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b, strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code, kbd, pre, samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button, input, optgroup, select, textarea {
+ color: inherit;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+ margin: 0;
+ /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+ text-transform: none;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button, html input[type="button"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */
+}
+
+input {
+ &[type="reset"], &[type="submit"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */
+ }
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled], html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+input {
+ &::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+ }
+ line-height: normal;
+ &[type="checkbox"], &[type="radio"] {
+ box-sizing: border-box;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+ }
+ &[type="number"] {
+ &::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
+ height: auto;
+ }
+ }
+ &[type="search"] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ /* 2 */
+ box-sizing: content-box;
+ &::-webkit-search-cancel-button, &::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ }
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td, th {
+ padding: 0;
+}
diff --git a/web-ui/app/scss/vendor/_scut.scss b/web-ui/app/scss/vendor/_scut.scss
new file mode 100644
index 00000000..3e16fa65
--- /dev/null
+++ b/web-ui/app/scss/vendor/_scut.scss
@@ -0,0 +1,1518 @@
+/*
+* Scut, a collection of Sass utilities
+* to ease and improve our implementations of common style-code patterns.
+* v1.3.0
+* Docs at http://davidtheclark.github.io/scut
+*/
+
+@mixin scut-clearfix {
+
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+
+}
+
+%scut-clearfix {
+ @include scut-clearfix;
+}
+@mixin scut-list-unstyled(
+ $no-margin: true
+) {
+
+ list-style-type: none;
+ padding-left: 0;
+
+ @if $no-margin {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+}
+
+%scut-list-unstyled {
+ @include scut-list-unstyled();
+}
+// Depends on `list-unstyled` and `clearfix`.
+
+@mixin scut-list-floated (
+ $space: false,
+ $dir: left,
+ $no-margin: true
+) {
+
+ @include scut-list-unstyled($no-margin);
+ @include scut-clearfix;
+
+ & > li {
+ float: $dir;
+ }
+
+ @if $space {
+ & > li + li {
+ margin-#{$dir}: $space;
+ }
+ }
+
+}
+
+%scut-list-floated {
+ @include scut-list-floated;
+}
+
+@function scut-autoOrValue ($val) {
+ @if $val == a or $val == auto {
+ @return auto;
+ }
+ @else {
+ @return $val;
+ }
+}
+
+@mixin scut-coords (
+ $coordinates: n n n n
+) {
+
+ $top: nth($coordinates, 1);
+ $right: nth($coordinates, 2);
+ $bottom: nth($coordinates, 3);
+ $left: nth($coordinates, 4);
+
+ @if $top != n {
+ top: scut-autoOrValue($top);
+ }
+ @if $right != n {
+ right: scut-autoOrValue($right);
+ }
+ @if $bottom != n {
+ bottom: scut-autoOrValue($bottom);
+ }
+ @if $left != n {
+ left: scut-autoOrValue($left);
+ }
+
+}
+@function scut-strip-unit (
+ $num
+) {
+
+ @return $num / ($num * 0 + 1);
+
+}
+// Depends on `scut-strip-unit`.
+
+$scut-em-base: 16 !default;
+
+@function scut-em (
+ $pixels,
+ $base: $scut-em-base
+) {
+
+ // $base could be in em or px (no unit = px).
+ // Adjust accordingly to create a $divisor that
+ // serves as context for $pixels.
+ $multiplier: if(unit($base) == em, 16, 1);
+ $divisor: scut-strip-unit($base) * $multiplier;
+
+ $em-vals: ();
+ @each $val in $pixels {
+ $val-in-ems: (scut-strip-unit($val) / $divisor) * 1em;
+ $em-vals: append($em-vals, $val-in-ems);
+ }
+
+ @if length($em-vals) == 1 {
+ // return a single value instead of a list,
+ // so it can be used in calculations
+ @return nth($em-vals, 1);
+ }
+ @else {
+ @return $em-vals;
+ }
+
+}
+// Depends on `scut-strip-unit`.
+
+$scut-rem-base: 16 !default;
+
+@function scut-rem (
+ $pixels
+) {
+
+ $rem-vals: ();
+ @each $val in $pixels {
+ $val-in-rems: scut-strip-unit($val) / $scut-rem-base * 1rem;
+ $rem-vals: append($rem-vals, $val-in-rems);
+ }
+
+ @if length($rem-vals) == 1 {
+ // return a single value instead of a list,
+ // so it can be used in calculations
+ @return nth($rem-vals, 1);
+ }
+ @else {
+ @return $rem-vals;
+ }
+
+}
+@mixin scut-border (
+ $style,
+ $sides: n y
+) {
+
+ @if length($sides) == 2 {
+ @if nth($sides, 1) != n {
+ border-top: $style;
+ border-bottom: $style;
+ }
+ @if nth($sides, 2) != n {
+ border-left: $style;
+ border-right: $style;
+ }
+ }
+
+ @else if length($sides) == 4 {
+ @if nth($sides, 1) != n {
+ border-top: $style;
+ }
+ @if nth($sides, 2) != n {
+ border-right: $style;
+ }
+ @if nth($sides, 3) != n {
+ border-bottom: $style;
+ }
+ @if nth($sides, 4) != n {
+ border-left: $style;
+ }
+ }
+
+ @else {
+ @warn "Scut-border requires a $sides argument of 2 or 4 values."
+ }
+
+}
+@mixin scut-circle (
+ $size,
+ $color: inherit
+) {
+
+ border-radius: 50%;
+ display: inline-block;
+
+ @if $color == inherit {
+ // If user wants to inherit the color,
+ // take advantage of the fact that border
+ // color defaults to the text color of the element.
+ border-width: $size / 2;
+ border-style: solid;
+ height: 0;
+ width: 0;
+ }
+ @else {
+ // Otherwise, just use background-color.
+ background-color: $color;
+ height: $size;
+ width: $size;
+ }
+
+}
+@mixin scut-color-swap (
+ $off,
+ $on,
+ $duration: 0,
+ $bg: false
+) {
+
+ $transition-properties: null;
+ $off-is-list: type-of($off) == list;
+ $on-is-list: type-of($on) == list;
+
+ // If $off IS a list,
+ // assign color and background-color.
+ @if $off-is-list {
+ color: nth($off, 1);
+ background-color: nth($off, 2);
+ $transition-properties: background-color, color;
+ }
+
+ // If $off IS NOT a list and $bg is TRUE,
+ // assign background-color.
+ @else if $bg and not($off-is-list) {
+ background-color: $off;
+ $transition-properties: background-color;
+ }
+
+ // If $off IS NOT a list and $bg is FALSE,
+ // assign color.
+ @else {
+ color: $off;
+ $transition-properties: color;
+ }
+
+ // Only set-up transition if $duration != 0.
+ @if $duration != 0 {
+ transition-property: $transition-properties;
+ transition-duration: $duration;
+ }
+
+ &:hover,
+ &:focus {
+
+ // $on is treated the same as $off, above.
+ @if $on-is-list {
+ color: nth($on, 1);
+ background-color: nth($on, 2);
+ }
+
+ @else if $bg and not($on-is-list) {
+ background-color: $on;
+ }
+
+ @else {
+ color: $on;
+ }
+ }
+
+}
+@mixin scut-hd-bp (
+ $ratio: 1.3
+) {
+
+ @media (-o-min-device-pixel-ratio: ($ratio / 1)),
+ (-webkit-min-device-pixel-ratio: $ratio),
+ (min-resolution: (round(96 * $ratio) * 1dpi)) {
+ @content;
+ }
+
+}
+
+@mixin scut-hide-visually {
+
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+
+}
+
+%scut-hide-visually {
+ @include scut-hide-visually;
+}
+@mixin scut-image-replace {
+
+ text-indent: 102%;
+ white-space: nowrap;
+ overflow: hidden;
+ padding: 0;
+
+}
+
+%scut-image-replace {
+ @include scut-image-replace;
+}
+
+// Depends on scut-rem and scut-strip-unit
+
+@mixin scut-rem-fallback (
+ $pixels,
+ $property: font-size
+) {
+
+ $px-vals: null;
+ @each $val in $pixels {
+ $val-in-px: scut-strip-unit($val) * 1px;
+ $px-vals: append($px-vals, $val-in-px);
+ }
+ $rem-vals: scut-rem($pixels);
+
+ #{$property}: $px-vals;
+ #{$property}: $rem-vals;
+
+}
+@mixin scut-reset-border-box {
+ // Make everything a border-box, because why not?
+ html {
+ box-sizing: border-box;
+ }
+ *, *:before, *:after {
+ box-sizing: inherit;
+ }
+}
+
+@mixin scut-reset-antialias {
+ // Antialias!
+ body {
+ -webkit-font-smoothing: antialiased;
+ }
+ *, *:before, *:after {
+ -webkit-font-smoothing: inherit;
+ }
+}
+
+@mixin scut-reset-semanticize {
+ // Make headers and <b> semantic, not presentational.
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-size: 1em;
+ font-weight: normal;
+ margin: 0;
+ }
+ b {
+ font-weight: normal;
+ }
+}
+
+@mixin scut-reset-pointer {
+ // Clickable form elements should have a pointer.
+ label,
+ select,
+ option,
+ button {
+ cursor: pointer;
+ }
+}
+
+@mixin scut-reset-form {
+ fieldset {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ }
+ textarea {
+ resize: vertical;
+ }
+}
+
+@mixin scut-reset-button {
+ // Reset default button styles, which are never used.
+ button,
+ input[type="button"],
+ input[type="submit"],
+ input[type="reset"] {
+ background: transparent;
+ border: 0;
+ color: inherit;
+ font: inherit;
+ margin: 0;
+ padding: 0;
+ width: auto;
+ -webkit-appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ &::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ }
+}
+
+@mixin scut-reset-paragraph {
+ // Some paragraph margins just get in the way.
+ p:first-of-type {
+ margin-top: 0;
+ }
+ p:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+@mixin scut-reset-media {
+ // You want these elements fluid, probably.
+ img,
+ video {
+ max-width: 100%;
+ height: auto;
+ }
+}
+
+@mixin scut-reset-figure {
+ // Remove default margins.
+ figure {
+ margin: 0;
+ }
+}
+
+// Call them all, minus exclusions!
+@mixin scut-reset ($exclude: false) {
+ @if not(index($exclude, border-box)) {
+ @include scut-reset-border-box;
+ }
+ @if not(index($exclude, antialias)) {
+ @include scut-reset-antialias;
+ }
+ @if not(index($exclude, semanticize)) {
+ @include scut-reset-semanticize;
+ }
+ @if not(index($exclude, pointer)) {
+ @include scut-reset-pointer;
+ }
+ @if not(index($exclude, form)) {
+ @include scut-reset-form;
+ }
+ @if not(index($exclude, button)) {
+ @include scut-reset-button;
+ }
+ @if not(index($exclude, paragraph)) {
+ @include scut-reset-paragraph;
+ }
+ @if not(index($exclude, media)) {
+ @include scut-reset-media;
+ }
+ @if not(index($exclude, figure)) {
+ @include scut-reset-figure;
+ }
+}
+
+@mixin scut-selected (
+ $active: false
+) {
+
+ @if $active {
+ &:hover,
+ &:focus,
+ &:active {
+ @content;
+ }
+ }
+ @else {
+ &:hover,
+ &:focus {
+ @content;
+ }
+ }
+
+}
+@mixin scut-triangle (
+ $direction: right,
+ $size: 0.75em,
+ $color: inherit
+) {
+
+ display: inline-block;
+ height: 0;
+ width: 0;
+ // For improved appearance in some Webkit browsers
+ -webkit-transform: rotate(360deg);
+
+ // Set up some variables
+ $width: null;
+ $height: null;
+ $border-widths: null;
+
+ @if type-of($size) == list {
+ $width: nth($size, 1);
+ $height: nth($size, 2);
+ }
+ @else {
+ $width: $size;
+ $height: $size;
+ }
+
+ @if ($direction == up) or ($direction == down) {
+ // For up and down, width gets two borders but height only one,
+ // so divide second border-width value by 2
+ $border-widths: $height ($width / 2);
+ }
+ @else if ($direction == right) or ($direction == left) {
+ // For right and left, height gets two borders but width only one,
+ // so divide first border-width value by 2
+ $border-widths: ($height / 2) $width;
+ }
+ @else {
+ // For right triangles (the rest), both sides get two borders,
+ // so divide both by 2
+ $border-widths: ($height / 2) ($width / 2);
+ }
+
+ border-width: $border-widths;
+ border-style: solid;
+
+
+ // STANDARD TRIANGLES
+
+ @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) {
+ border-color: transparent;
+ @if $direction == up {
+ border-bottom-color: $color;
+ border-top-width: 0;
+ }
+ @else if $direction == right {
+ border-left-color: $color;
+ border-right-width: 0;
+ }
+ @else if $direction == down {
+ border-top-color: $color;
+ border-bottom-width: 0;
+ }
+ @else if $direction == left {
+ border-right-color: $color;
+ border-left-width: 0;
+ }
+ }
+
+
+ // CORNER TRIANGLES
+
+ @else if ($direction == top-right) or ($direction == top-left) {
+ border-top-color: $color;
+ border-bottom-color: transparent;
+ @if $direction == top-right {
+ border-left-color: transparent;
+ border-right-color: $color;
+ }
+ @else if $direction == top-left {
+ border-left-color: $color;
+ border-right-color: transparent;
+ }
+ }
+
+ @else if ($direction == bottom-right) or ($direction == bottom-left) {
+ border-top-color: transparent;
+ border-bottom-color: $color;
+ @if $direction == bottom-right {
+ border-left-color: transparent;
+ border-right-color: $color;
+ }
+ @else if $direction == bottom-left {
+ border-left-color: $color;
+ border-right-color: transparent;
+ }
+ }
+
+}
+
+%scut-triangle {
+ @include scut-triangle;
+}
+@mixin scut-center-absolutely (
+ $dimensions
+) {
+
+ $width: nth($dimensions, 1);
+ $height: nth($dimensions, 2);
+
+ position: absolute;
+
+ @if $width != n {
+ width: $width;
+ left: 50%;
+ margin-left: (-$width / 2);
+ }
+
+ @if $height != n {
+ height: $height;
+ top: 50%;
+ margin-top: (-$height / 2);
+ }
+
+}
+@mixin scut-center-block (
+ $max-width: false
+) {
+
+ margin-left: auto;
+ margin-right: auto;
+ @if $max-width {
+ max-width: $max-width;
+ }
+
+}
+
+%scut-center-block {
+ @include scut-center-block;
+}
+
+@mixin scut-center-transform (
+ $axis: false // or x or y
+) {
+
+ position: absolute;
+
+ @if $axis != x {
+ top: 50%;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+
+ @if $axis != y {
+ left: 50%;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ $translate-val: null;
+
+ @if not($axis) {
+ $translate-val: translate(-50%, -50%);
+ }
+ @else if $axis != x {
+ $translate-val: translateY(-50%);
+ }
+ @else if $axis != y {
+ $translate-val: translateX(-50%);
+ }
+
+ -webkit-transform: $translate-val;
+ -ms-transform: $translate-val;
+ transform: $translate-val;
+}
+
+%scut-center-transform {
+ @include scut-center-transform;
+}
+
+%scut-center-transform-x {
+ @include scut-center-transform(x);
+}
+
+%scut-center-transform-y {
+ @include scut-center-transform(y);
+}
+
+@mixin scut-fill (
+ $width-height: false
+) {
+
+ position: absolute;
+ left: 0;
+ top: 0;
+ @if $width-height {
+ width: 100%;
+ height: 100%;
+ }
+ @else {
+ right: 0;
+ bottom: 0;
+ }
+
+}
+
+%scut-fill {
+ @include scut-fill;
+}
+@mixin scut-list-custom (
+ $content: "\2022",
+ $marker-width: 0.75em,
+ $pad: 0,
+ $no-margin: false
+) {
+
+ $content-val: null;
+ $counter: index($content, count);
+ @if $counter {
+ @if length($content) == 3 {
+ $content-val: counter(scutlistcounter, nth($content, 3))nth($content,2);
+ }
+ @else if length($content) == 2 {
+ $content-val: counter(scutlistcounter)nth($content,2);
+ }
+ @else {
+ $content-val: counter(scutlistcounter);
+ }
+ }
+ @else {
+ $content-val: $content;
+ }
+
+ padding-left: $marker-width + $pad;
+ list-style-type: none;
+
+ @if $no-margin {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ & > li {
+ position: relative;
+ @if $counter {
+ counter-increment: scutlistcounter;
+ }
+ &:before {
+ content: $content-val;
+ display: block;
+ position: absolute;
+ top: 0;
+ left: -$marker-width;
+ width: $marker-width;
+ @content;
+ }
+ }
+
+}
+// Depends on `list-floated`, which depends in turn on `list-unstyled` and `clearfix`.
+
+@mixin scut-list-divided (
+ $divider: "|",
+ $space: 0.5em,
+ $dir: left,
+ $height: false,
+ $no-margin: true
+) {
+
+ @include scut-list-floated($dir: $dir, $no-margin: $no-margin);
+
+ $pseudo: if($dir == left, 'before', 'after');
+
+ // If an explicit height is passed,
+ // things are different: All <li>s
+ // need the pseudo-element (to force height),
+ // but the first's must be hidden.
+
+ @if $height {
+ & > li {
+ height: $height;
+ }
+ & > li:#{$pseudo} {
+ height: $height;
+ content: $divider;
+ display: inline-block;
+ vertical-align: middle;
+ @content;
+ }
+ & > li:first-child:#{$pseudo} {
+ width: 0;
+ overflow: hidden;
+ }
+ }
+
+ & > li + li:#{$pseudo} {
+ @if not($height) {
+ content: $divider;
+ display: inline-block;
+ @content;
+ }
+ margin-left: $space;
+ margin-right: $space;
+ }
+
+}
+
+%scut-list-bar {
+ @include scut-list-divided;
+}
+
+%scut-list-breadcrumb {
+ @include scut-list-divided("/");
+}
+// Depends on `list-unstyled`.
+
+@mixin scut-list-inline (
+ $space: false,
+ $no-margin: true
+) {
+
+ @include scut-list-unstyled($no-margin);
+
+ & > li {
+ display: inline-block;
+ }
+
+ @if $space {
+ & > li + li {
+ margin-left: $space;
+ }
+ }
+
+}
+
+%scut-list-inline {
+ @include scut-list-inline;
+}
+// Depends on `list-unstyled`.
+
+@mixin scut-list-punctuated (
+ $divider: ", ",
+ $display: inline,
+ $no-margin: true
+) {
+
+ @include scut-list-unstyled($no-margin);
+
+ & > li {
+ display: $display;
+ &:not(:last-child):after {
+ content: $divider;
+ }
+ }
+
+}
+
+%scut-list-comma {
+ @include scut-list-punctuated;
+}
+@mixin scut-margin (
+ $margin
+) {
+
+ @if length($margin) == 1 and $margin != n {
+ margin-top: $margin;
+ margin-right: $margin;
+ margin-bottom: $margin;
+ margin-left: $margin;
+ }
+
+ @if length($margin) == 2 {
+ $margin-y: nth($margin, 1);
+ $margin-x: nth($margin, 2);
+ @if $margin-y != n {
+ margin-top: $margin-y;
+ margin-bottom: $margin-y;
+ }
+ @if $margin-x != n {
+ margin-left: $margin-x;
+ margin-right: $margin-x;
+ }
+ }
+
+ @if length($margin) == 3 {
+ $margin-y-top: nth($margin, 1);
+ $margin-x: nth($margin, 2);
+ $margin-y-bottom: nth($margin, 3);
+ @if $margin-y-top != n {
+ margin-top: $margin-y-top;
+ }
+ @if $margin-x != n {
+ margin-right: $margin-x;
+ margin-left: $margin-x;
+ }
+ @if $margin-y-bottom != n {
+ margin-bottom: $margin-y-bottom;
+ }
+ }
+
+ @if length($margin) == 4 {
+ $margin-top: nth($margin, 1);
+ $margin-right: nth($margin, 2);
+ $margin-bottom: nth($margin, 3);
+ $margin-left: nth($margin, 4);
+ @if $margin-top != n {
+ margin-top: $margin-top;
+ }
+ @if $margin-right != n {
+ margin-right: $margin-right;
+ }
+ @if $margin-bottom != n {
+ margin-bottom: $margin-bottom;
+ }
+ @if $margin-left != n {
+ margin-left: $margin-left;
+ }
+ }
+
+}
+@mixin scut-padding (
+ $padding
+) {
+
+ @if length($padding) == 1 and $padding != n {
+ padding-top: $padding;
+ padding-right: $padding;
+ padding-bottom: $padding;
+ padding-left: $padding;
+ }
+
+ @if length($padding) == 2 {
+ $padding-y: nth($padding, 1);
+ $padding-x: nth($padding, 2);
+ @if $padding-y != n {
+ padding-top: $padding-y;
+ padding-bottom: $padding-y;
+ }
+ @if $padding-x != n {
+ padding-left: $padding-x;
+ padding-right: $padding-x;
+ }
+ }
+
+ @if length($padding) == 3 {
+ $padding-y-top: nth($padding, 1);
+ $padding-x: nth($padding, 2);
+ $padding-y-bottom: nth($padding, 3);
+ @if $padding-y-top != n {
+ padding-top: $padding-y-top;
+ }
+ @if $padding-x != n {
+ padding-right: $padding-x;
+ padding-left: $padding-x;
+ }
+ @if $padding-y-bottom != n {
+ padding-bottom: $padding-y-bottom;
+ }
+ }
+
+ @if length($padding) == 4 {
+ $padding-top: nth($padding, 1);
+ $padding-right: nth($padding, 2);
+ $padding-bottom: nth($padding, 3);
+ $padding-left: nth($padding, 4);
+ @if $padding-top != n {
+ padding-top: $padding-top;
+ }
+ @if $padding-right != n {
+ padding-right: $padding-right;
+ }
+ @if $padding-bottom != n {
+ padding-bottom: $padding-bottom;
+ }
+ @if $padding-left != n {
+ padding-left: $padding-left;
+ }
+ }
+}
+// Depends on `positioning-coordinates`.
+
+@mixin scut-absolute (
+ $coordinates: 0 n n 0
+) {
+
+ position: absolute;
+ @include scut-coords($coordinates);
+
+}
+
+%scut-absolute {
+ @include scut-absolute;
+}
+// Depends on `positioning-coordinates`.
+
+@mixin scut-fixed (
+ $coordinates: 0 n n 0
+) {
+
+ position: fixed;
+ @include scut-coords($coordinates);
+
+}
+
+%scut-fixed {
+ @include scut-fixed;
+}
+// Depends on `positioning-coordinates`.
+
+@mixin scut-relative (
+ $coordinates: n n n n
+) {
+
+ position: relative;
+ @include scut-coords($coordinates);
+
+}
+@mixin scut-ratio-box (
+ $ratio: 1/1
+) {
+
+ overflow: hidden;
+ position: relative;
+
+ // The container's height, as a percentage of the
+ // container's width, is set by assigning
+ // padding-top to a pseudo-element.
+ &:before {
+ content: "";
+ display: block;
+ height: 0;
+ padding-top: (1 / $ratio) * 100%;
+ }
+
+}
+
+%scut-ratio-box {
+ @include scut-ratio-box;
+}
+@mixin scut-size(
+ $size
+) {
+
+ @if length($size) == 1 {
+ width: $size;
+ height: $size;
+ }
+ @else if length($size) == 2 {
+ width: nth($size, 1);
+ height: nth($size, 2);
+ }
+
+}
+@mixin scut-sticky-footer-fixed (
+ $height,
+ $wrapper: ".wrapper",
+ $footer: ".scut-sticky"
+) {
+
+ html,
+ body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ }
+
+ #{$wrapper} {
+ min-height: 100%;
+ margin-bottom: -$height;
+ &:after {
+ content: "";
+ display: block;
+ }
+ }
+
+ #{$wrapper}:after,
+ #{$footer} {
+ height: $height;
+ }
+
+}
+
+// deprecated
+@mixin scut-sticky-footer (
+ $height,
+ $wrapper: ".wrapper",
+ $footer: ".scut-sticky"
+){
+ @include scut-sticky-footer-fixed($height, $wrapper, $footer);
+}
+@mixin scut-sticky-footer-fluid (
+ $wrapper: ".wrapper",
+ $footer: ".scut-sticky"
+) {
+
+ html,
+ body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ }
+
+ #{$wrapper} {
+ display: table;
+ height: 100%;
+ width: 100%;
+ }
+
+ #{$footer} {
+ display: table-row;
+ height: 1px;
+ }
+
+}
+@mixin scut-vcenter-ib (
+ $inner...
+) {
+
+ // The inner element is vertically centered
+ // by middle-aligning it with an inline pseudo-element
+ // whose height is 100%.
+
+ &:before {
+ content: "";
+ height: 100%;
+ display: inline-block;
+ vertical-align: middle;
+ // A small negative right margin is set
+ // to account for the default
+ // word-spacing of inline-block.
+ margin-right: -0.25em;
+ }
+
+ $inner: if(length($inner) == 0, ".scut-inner", $inner);
+ @each $cell-selector in $inner {
+ $cell-selector: unquote($cell-selector);
+ & > #{$cell-selector} {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+
+}
+
+%scut-vcenter-ib {
+ @include scut-vcenter-ib;
+}
+
+@mixin scut-vcenter-lh (
+ $height
+) {
+
+ height: $height;
+ line-height: $height;
+
+}
+@mixin scut-vcenter-td (
+ $inner...
+) {
+
+ display: table;
+
+ $inner: if(length($inner) == 0, ".scut-inner", $inner);
+ @each $cell-selector in $inner {
+ $cell-selector: unquote($cell-selector);
+ & > #{$cell-selector} {
+ display: table-cell;
+ vertical-align: middle;
+ }
+ }
+
+}
+
+
+%scut-vcenter-td {
+ @include scut-vcenter-td;
+}
+
+// Depends on scut-center-transform
+
+@mixin scut-vcenter-tt () {
+ @include scut-center-transform(y);
+}
+
+%scut-vcenter-tt {
+ @include scut-vcenter-tt;
+}
+// space
+$scut-space: "\0020";
+// non-breaking space
+$scut-nbsp: "\00a0";
+
+// quotation mark
+$scut-quot: "\0022";
+// left single curly quote
+$scut-lsquo: "\2018";
+// right single curly quote
+$scut-rsquo: "\2019";
+// left double curly quote
+$scut-ldquo: "\201C";
+// right double curly quote
+$scut-rdquo: "\201D";
+// left single angle quote (guillemet)
+$scut-lsaquo: "\2039";
+// right single angle quote (guillemet)
+$scut-rsaquo: "\203A";
+// left double angle quote (guillemet)
+$scut-laquo: "\00ab";
+// right double angle quote (guillemet)
+$scut-raquo: "\00bb";
+
+// em dash (mutton)
+$scut-mdash: "\2014";
+// en dash (nut)
+$scut-ndash: "\2013";
+// hyphen
+$scut-hyphen: "\2010";
+
+// ampersand
+$scut-amp: "\0026";
+// greater than
+$scut-gt: "\003e";
+// less than
+$scut-lt: "\003c";
+// times
+$scut-times: "\00D7";
+// big times
+$scut-bigtimes: "\2715";
+// checkmark
+$scut-checkmark: "\2713";
+
+// section sign (double S, hurricane, sectional symbol, the legal doughnut, signum sectionis)
+$scut-sect: "\00a7";
+// paragraph symbol (pilcrow)
+$scut-para: "\00b6";
+
+// middot (interpunct, interpoint)
+$scut-middot: "\00b7";
+// o-slash (slashed o)
+$scut-oslash: "\00f8";
+// bullet
+$scut-bull: "\2022";
+// white bullet
+$scut-whibull: "\25E6";
+// horizontal ellipsis
+$scut-hellip: "\2026";
+// vertical ellipsis
+$scut-vellip: "\22EE";
+// midline horizontal ellipsis
+$scut-midhellip: "\22EF";
+
+// up-pointing triangle
+$scut-utri: "\25b2";
+// down-pointing triangle
+$scut-dtri: "\25bc";
+// left-pointing triangle
+$scut-ltri: "\25c0";
+// right-pointing triangle
+$scut-rtri: "\25b6";
+// up-pointing small triangle
+$scut-ustri: "\25b4";
+// down-pointing small triangle
+$scut-dstri: "\25be";
+// left-pointing small triangle
+$scut-lstri: "\25c2";
+// right-pointing small triangle
+$scut-rstri: "\25b8";
+// diamond
+$scut-diamond: "\25c6";
+// fisheye
+$scut-fisheye: "\25c9";
+// bullseye
+$scut-bullseye: "\25ce";
+// circle
+$scut-circle: "\25cf";
+// white circle
+$scut-whitecircle: "\25cb";
+// square
+$scut-square: "\25a0";
+// white square
+$scut-whitesquare: "\25a1";
+// small square
+$scut-ssquare: "\25aa";
+// small white square
+$scut-swhitesquare: "\25ab";
+@function main-src($formats, $file-path, $font-family) {
+ // Return the list of `src` values, in order, that
+ // a good `@font-face` will need, including only
+ // those formats specified in the list `$formats`.
+ $result: ();
+ @if index($formats, eot) {
+ $eot-val: url('#{$file-path}.eot?#iefix') format('embedded-opentype');
+ $result: append($result, $eot-val, comma);
+ }
+ @if index($formats, woff2) {
+ $woff2-val: url('#{$file-path}.woff2') format('woff2');
+ $result: append($result, $woff2-val, comma);
+ }
+ @if index($formats, woff) {
+ $woff-val: url('#{$file-path}.woff') format('woff');
+ $result: append($result, $woff-val, comma);
+ }
+ @if index($formats, ttf) {
+ $ttf-val: url('#{$file-path}.ttf') format('truetype');
+ $result: append($result, $ttf-val, comma);
+ }
+ @if index($formats, svg) {
+ $svg-val: url('#{$file-path}.svg##{$font-family}') format('svg');
+ $result: append($result, $svg-val, comma);
+ }
+ @return $result;
+}
+
+@mixin scut-font-face (
+ $font-family,
+ $file-path,
+ $weight: normal,
+ $style: normal,
+ $formats: eot woff2 woff ttf svg
+) {
+
+ @if index('italic' 'oblique', $weight) {
+ $style: $weight;
+ $weight: normal;
+ }
+
+ @font-face {
+ font-family: $font-family;
+ font-weight: $weight;
+ font-style: $style;
+
+ @if index($formats, eot) {
+ src: url('#{$file-path}.eot');
+ }
+ src: main-src($formats, $file-path, $font-family);
+ }
+
+}
+
+@mixin scut-hanging-indent (
+ $indent: 1em
+) {
+
+ // padding-left creates the indent,
+ // while text-indent pulls the first line
+ // back to the edge.
+
+ padding-left: $indent;
+ text-indent: -$indent;
+
+}
+
+%scut-hanging-indent {
+ @include scut-hanging-indent;
+}
+@mixin scut-indented-ps (
+ $indent: 1.5em,
+ $no-first-indent: true
+) {
+
+ p {
+ margin: 0;
+ text-indent: $indent;
+ }
+
+ @if $no-first-indent {
+ p:first-of-type {
+ text-indent: 0;
+ }
+ }
+
+}
+
+%scut-indented-ps {
+ @include scut-indented-ps;
+}
+@mixin scut-key-val (
+ $divider: ":",
+ $pad: 0.25em,
+ $indent: 1em,
+ $spacing: 0,
+ $pad-left: 0
+) {
+
+ & > dt {
+ clear: both;
+ float: left;
+ &:after {
+ content: $divider;
+ margin-right: $pad;
+ @if $pad-left != 0 {
+ margin-left: $pad-left;
+ }
+ }
+ }
+
+ & > dd {
+ margin-left: $indent;
+ @if $spacing != 0 {
+ margin-bottom: $spacing;
+ }
+ }
+
+}
+
+%scut-key-val {
+ @include scut-key-val;
+}
+@mixin scut-link-bb (
+ $color: inherit,
+ $style: solid,
+ $width: 1px
+) {
+
+ text-decoration: none;
+
+ border-bottom-width: $width;
+ border-bottom-style: $style;
+ @if $color != inherit {
+ border-bottom-color: $color;
+ }
+
+}
+
+%scut-link-bb {
+ @include scut-link-bb;
+}
+// SCUT LINK UNSTYLED
+// http://davidtheclark.github.io/scut/#link-unstyled
+
+@mixin scut-link-unstyled() {
+
+ text-decoration: none;
+ color: inherit;
+
+}
+
+%scut-link-unstyled {
+ @include scut-link-unstyled();
+}
+
+@mixin scut-reverse-italics (
+ $elements: null
+) {
+
+ $element-list: em, cite, i;
+ font-style: italic;
+ #{join($element-list, $elements)} {
+ font-style: normal;
+ }
+
+}
+
+%scut-reverse-italics {
+ @include scut-reverse-italics;
+}
+
+@mixin scut-side-lined (
+ $height: 1px,
+ $space: 0.5em,
+ $color: inherit,
+ $style: solid,
+ $v-adjust: false,
+ $double: false
+) {
+
+ display: block;
+ overflow: hidden;
+ text-align: center;
+
+ &:before,
+ &:after {
+ content: "";
+ display: inline-block;
+ vertical-align: middle;
+ position: relative;
+ width: 50%;
+
+ border-top-style: $style;
+ border-top-width: $height;
+
+ @if $color != inherit {
+ border-top-color: $color;
+ }
+
+ @if $v-adjust != false {
+ bottom: $v-adjust;
+ }
+
+ @if $double != false {
+ height: $double;
+ border-bottom-style: $style;
+ border-bottom-width: $height;
+ @if $color != inherit {
+ border-bottom-color: $color;
+ }
+ }
+ }
+
+ &:before {
+ right: $space;
+ margin-left: -50%;
+ }
+ &:after {
+ left: $space;
+ margin-right: -50%;
+ }
+
+}
+
+%scut-side-lined {
+ @include scut-side-lined;
+}
+@mixin scut-truncate {
+
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+}
+
+%scut-truncate {
+ @include scut-truncate;
+} \ No newline at end of file
diff --git a/web-ui/app/scss/views/_action-bar.scss b/web-ui/app/scss/views/_action-bar.scss
new file mode 100644
index 00000000..40e677b0
--- /dev/null
+++ b/web-ui/app/scss/views/_action-bar.scss
@@ -0,0 +1,159 @@
+#top-pane {
+ height: auto;
+ overflow: hidden;
+ background: $top_pane;
+ border-top: 1px solid $top_pane;
+
+ #list-actions {
+ width: 100%;
+ height: 34px;
+ margin: 0;
+ border-top: 1px solid $white;
+ border-bottom: 2px solid lighten($top_pane, 30%);
+ background: $white;
+ clear: both;
+ overflow: hidden;
+ padding-left: 10px;
+
+ li {
+ display: inline-block;
+ margin: 1px -3px;
+ vertical-align: top;
+
+ input[type=checkbox] {
+ @include check-box;
+
+ margin: 7px 13px 7px;
+ }
+
+ select {
+ padding: 1px 3px;
+ margin: 0;
+ }
+
+ input[type=button] {
+ margin: 2px;
+ padding: 4px 10px;
+ background: $background_light_grey;
+ color: $dark_grey;
+ text-transform: uppercase;
+ font-weight: 400;
+ font-size: 0.8em;
+ opacity: 0.7;
+ border: 1px solid darken($contrast, 10%);
+
+ @include border-radius(1px);
+
+ @include btn-transition;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &[disabled=disabled] {
+ opacity: 0.5;
+ cursor: default;
+ }
+ }
+ }
+
+ #pagination-trigger {
+ cursor: pointer;
+ margin: 4px 12px 0 5px;
+
+ span {
+ padding-left: 5px;
+ }
+ }
+ }
+
+ #compose-search-trigger {
+ padding: 4px;
+ }
+
+ #actions {
+ ul {
+ margin: 0;
+
+ li {
+ display: inline-block;
+ margin-right: -5px;
+
+ a {
+ transition: background-color 150ms ease-out;
+ background: $top_pane;
+ color: $white;
+ font-size: 1.5em;
+ display: block;
+ padding: 14px 20px;
+ margin: 0 1px 0px;
+ opacity: 0.35;
+
+ &.selected {
+ background: $top_pane;
+ opacity: 1;
+ cursor: default;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+
+ #search-trigger {
+ padding: 5px;
+ padding-left: 0;
+
+ input {
+ margin: 0;
+ padding: 8px 30px;
+ color: $navigation_background;
+ background: white;
+ border: none;
+ transition: background-color 150ms ease-out;
+
+ &:hover {
+ background: darken(white, 2%);
+ }
+
+ &:focus {
+ background: darken(white, 5%);
+ }
+ }
+
+ form:before {
+ font-family: "FontAwesome";
+ content: "\f002";
+ position: absolute;
+ padding: 0 10px;
+ top: 15px;
+ color: $medium_light_grey;
+ }
+ }
+}
+
+#refresh-mails-trigger {
+ i {
+ margin-top: 3px;
+ cursor: pointer;
+ opacity: 0.9;
+ padding: 4px;
+
+ &:hover {
+ opacity: 1;
+
+ &:after {
+ content: "\f021";
+ }
+
+ &:before {
+ content: attr(data-label);
+ font-size: 0.8em;
+ padding-right: 5px;
+ }
+ }
+ }
+}
diff --git a/web-ui/app/scss/views/_close-button.scss b/web-ui/app/scss/views/_close-button.scss
new file mode 100644
index 00000000..37171c18
--- /dev/null
+++ b/web-ui/app/scss/views/_close-button.scss
@@ -0,0 +1,22 @@
+.close-mail-button {
+ $button-size: 27px;
+
+ margin-right: 3px;
+ float: left;
+ background: $lighter_gray;
+ color: $medium_light_grey;
+ width: $button-size;
+ height: $button-size;
+ padding: 0;
+ border-radius: 0;
+
+ &:hover, &:focus, &:active {
+ background-color: darken($lighter_gray, 2);
+ color: darken($medium_light_grey, 10);
+ }
+
+ i {
+ padding: 0;
+ margin: 0;
+ }
+}
diff --git a/web-ui/app/scss/views/_compose-button.scss b/web-ui/app/scss/views/_compose-button.scss
new file mode 100644
index 00000000..81e0bb33
--- /dev/null
+++ b/web-ui/app/scss/views/_compose-button.scss
@@ -0,0 +1,27 @@
+// COMPOSE BUTTON
+#compose {
+ margin-bottom: 5px;
+ padding-right: 4px;
+ #compose-trigger {
+ width: 100%;
+ display: inline-block;
+ padding: 5px;
+ #compose-mails-trigger {
+ background: $action_buttons;
+ color: $white;
+ padding: 10px 30px;
+ text-align: center;
+ font-weight: 400;
+ font-size: 1.2em;
+ width: 100%;
+ height: 100%;
+ margin-bottom: 0px;
+ @include btn-transition;
+ &:hover {
+ background: lighten($action_buttons, 10%);
+ cursor: pointer;
+ }
+ }
+ }
+}
+
diff --git a/web-ui/app/scss/views/_compose-view.scss b/web-ui/app/scss/views/_compose-view.scss
new file mode 100644
index 00000000..9e120357
--- /dev/null
+++ b/web-ui/app/scss/views/_compose-view.scss
@@ -0,0 +1,451 @@
+.compose-view {
+ overflow: auto;
+
+ &__buttons {
+ &-attachment {
+ cursor: pointer;
+ margin-left: 18px;
+ padding-top: 0px;
+ display: inline;
+ border: 1px $contrast solid;
+ background: $background_light_grey;
+ padding: 7px 4px;
+ font-size: 0.8em;
+
+ span {
+ -ms-transform: rotate(224deg);
+ -webkit-transform: rotate(224deg);
+ transform: rotate(224deg);
+ outline: 0;
+ }
+
+ i.fa-paperclip {
+ font-size: 1.7em;
+ }
+
+ &--busy {
+ color: lighten($recipients_font_color, 10%);
+ cursor: progress;
+ }
+ }
+ }
+
+ &__attachments {
+ &-wrapper {
+ padding: 0;
+ margin-top: 30px;
+ }
+
+ &-list {
+ &-item {
+ display: block;
+ position: relative;
+ margin-bottom: 8px;
+ padding: 5px;
+ border: 1px solid $border_light_grey;
+ border-radius: 2px;
+ background-color: $contrast;
+
+ &-label {
+ color: $attachment_text;
+ text-decoration: none;
+
+ &:hover, &:focus {
+ color: $attachment_icon;
+ outline: none;
+ }
+ }
+
+ &-icon {
+ color: #a2a2a2;
+ float: right;
+ margin-top: 7px;
+ cursor: pointer;
+ }
+
+ &-progress {
+ width: 0%;
+ position: absolute;
+ right: 0;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ min-height: 100%;
+
+ &-bar {
+ height: 100%;
+ background-color: rgba($light_blue, 0.3);
+ }
+ }
+
+ }
+
+ &--upload {
+ display: none;
+ }
+
+ }
+
+ &-error {
+ background-color: $background_light_grey;
+ border-radius: 2px;
+ border: 1px solid $error;
+ display: block;
+ font-size: 0.9rem;
+ margin-bottom: 20px;
+ padding: 5px;
+ width: 100%;
+
+ &-close {
+ float: left;
+ margin: 5px 5px 0 0;
+ }
+
+ & > * {
+ color: $error;
+ }
+
+ & > a {
+ display: inline-block;
+ text-decoration: underline;
+ padding: 5px;
+ }
+ }
+ }
+
+}
+
+// COMPOSE PANE
+#compose-box, #draft-box, #reply-box, #feedback-box {
+ div.floatlabel {
+ position: relative;
+ }
+
+ .input-container {
+ padding: 1px;
+ }
+
+ label, span {
+ color: $recipients_font_color;
+ padding: 0.5rem;
+ display: inline-block;
+ }
+
+ label {
+ padding: 13px 10px;
+ }
+
+ span {
+ padding: 3px;
+
+ &.attachment-size {
+ color: $attachment_size;
+ cursor: pointer;
+ }
+ }
+
+ label.floatlabel {
+ padding: 0.4rem !important;
+ position: absolute;
+ font-size: 0.6rem;
+ transition: all 0.1s linear;
+ opacity: 0;
+ font-weight: bold;
+ }
+
+ label.showfloatlabel {
+ color: $light_blue !important;
+ top: -0.3rem;
+ opacity: 1;
+ }
+
+ input, textarea {
+ margin: 0;
+ border: none;
+ transition: all 0.1s linear;
+ }
+
+ input.showfloatlabel, textarea.showfloatlabel {
+ padding-top: 1rem !important;
+ }
+
+ input#subject, #feedback-subject {
+ font-size: 1.6875rem;
+ line-height: 1.4;
+ border-top: 1px solid $lighter_gray;
+ }
+
+ #feedback-subject {
+ color: $dark_grey;
+ }
+
+ textarea {
+ border-bottom: 2px solid $lighter_gray;
+ min-height: 400px;
+ font-family: inherit;
+ font-weight: normal;
+ font-size: 1rem;
+ line-height: 1.6;
+ text-rendering: optimizeLegibility;
+ }
+
+ &.reply-box, &.forward-box {
+ margin: 0;
+
+ h4 {
+ font-size: 0.9em;
+ font-style: italic;
+ color: $medium_grey;
+ margin: 2px 0;
+ clear: both;
+ cursor: pointer;
+
+ &:hover {
+ background: $contrast;
+ }
+ }
+
+ textarea {
+ min-height: 200px;
+ margin: 10px 0;
+ }
+
+ p {
+ padding: 5px;
+ margin: 10px 0;
+ font-style: italic;
+ cursor: pointer;
+
+ &:hover {
+ background: $contrast;
+ }
+ }
+ }
+
+ button.close-mail-button {
+ margin: 1px;
+ }
+
+ .buttons-group {
+ margin-top: 0px;
+ }
+
+ .recipients-area {
+ -webkit-appearance: none;
+ background-color: white;
+ font-family: inherit;
+ display: flex;
+ flex-wrap: wrap;
+ font-size: 0.898em;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: relative;
+
+ .compose-column-label {
+ width: 5%;
+ display: inline-block;
+ }
+
+ .compose-column-recipients {
+ width: 95%;
+ display: inline-block;
+ }
+
+ .recipients-label {
+ width: 100%;
+ height: 100%;
+ }
+
+ .recipients-navigation-handler {
+ z-index: -1;
+ position: absolute;
+ top: -200px;
+ }
+
+ .twitter-typeahead {
+ flex: 1 1 50px;
+
+ .tt-dropdown-menu {
+ background: $dark_white;
+
+ div div {
+ padding: 8px;
+
+ &:hover {
+ background: $background_dropdown_grey;
+ }
+ }
+ }
+ }
+
+ .invalid-format {
+ border-bottom: 1px dotted $error;
+ }
+
+ input[type=text] {
+ vertical-align: top;
+ height: 35px;
+ margin-left: 1px;
+ font-size: 0.9em;
+ width: 100%;
+ }
+
+ .fixed-recipient {
+ display: inline-block;
+ margin-right: -3px;
+ flex: none;
+ position: relative;
+
+ .recipient-value {
+ &.selected {
+ border: 1px solid $medium_dark_grey;
+ }
+
+ &:before {
+ font-family: FontAwesome;
+ padding-right: 6px;
+ font-size: 1.4em;
+ }
+
+ &.encrypted {
+ border-bottom-color: $will_be_encrypted;
+
+ &:before {
+ color: $will_be_encrypted;
+ content: "\f023 ";
+ }
+ }
+
+ &.not-encrypted {
+ border-bottom-color: $wont_be_encrypted;
+
+ &:before {
+ color: $wont_be_encrypted;
+ content: "\f09c";
+ }
+ }
+
+ &.deleting span {
+ text-decoration: line-through;
+ }
+
+ & span {
+ margin: 0px;
+ padding: 0px 0px 0px 0px;
+ vertical-align: top;
+ cursor: pointer;
+ }
+
+ margin: 3px;
+ padding: 5px;
+ background-color: $background_light_grey;
+ border: 1px solid $border_light_grey;
+ border-radius: 2px;
+ }
+
+ .recipient-del {
+ position: relative;
+ color: $recipients_font_color;
+
+ &:hover, &:focus {
+ color: $recipients_font_color;
+ }
+
+ &:before {
+ margin-left: 0.4em;
+ font-weight: bold;
+ content: "x";
+ }
+
+ &.deleteTooltip:hover:after {
+ position: absolute;
+ content: attr(data-label);
+ font-size: 0.5rem;
+
+ @include tooltip(25px, 0px);
+ }
+ }
+ }
+
+ input.recipients-input:focus {
+ background-color: $dark_white !important;
+ border-color: $medium_light_grey;
+ outline: none;
+ width: 270px;
+ }
+ }
+
+ .collapse {
+ display: block;
+ position: absolute;
+ right: 10px;
+ padding-right: 15px;
+ padding-left: 15px;
+ font-family: 'FontAwesome';
+ font-weight: bolder;
+ font-size: larger;
+ cursor: pointer;
+ }
+
+ .collapse + input, .collapse + input + * {
+ display: none;
+ }
+
+ .collapse + input:checked + * {
+ display: block;
+ }
+}
+
+#reply-section {
+ padding-left: 30px;
+
+ .reply-container {
+ margin: 10px 0;
+ padding: 10px;
+ border: 1px dashed darken($contrast, 10%);
+
+ @include btn-transition;
+ }
+
+ button {
+ margin: 0;
+ }
+
+ #all-recipients {
+ color: $black;
+ }
+
+ #all-recipients:focus {
+ background-color: darken($contrast, 10%);
+ }
+
+ #reply-button, #reply-all-button, #forward-button {
+ text-align: center;
+ font-weight: 100;
+ font-size: 1.1em;
+ background: $white;
+ color: $medium_light_grey;
+ padding: 25px;
+ margin: 0;
+
+ @include border-radius(0);
+
+ &:hover {
+ background: darken($contrast, 5%);
+ cursor: pointer;
+ }
+ }
+}
+
+.buttons-group {
+ clear: both;
+ margin: 20px 0 0;
+ padding: 0;
+}
+
+#draft-save-status {
+ float: right;
+ padding: 0.4rem 1.1rem;
+ color: $lighter_blue;
+}
diff --git a/web-ui/app/scss/views/_mail-list.scss b/web-ui/app/scss/views/_mail-list.scss
new file mode 100644
index 00000000..f5c4c60f
--- /dev/null
+++ b/web-ui/app/scss/views/_mail-list.scss
@@ -0,0 +1,124 @@
+.mail-list-entry {
+ @include scut-clearfix;
+
+ border-bottom: 1px solid white;
+ transition: background-color 150ms ease-out;
+ font-weight: bold;
+ height: 80px;
+ position: relative;
+
+ // Workaround:
+ // Foundation is of the opinion that a 1.6 line height for all lists
+ // is a totally good idea. Please remove when Foundation is gone
+ line-height: normal;
+
+ &.status-read {
+ font-weight: normal;
+ color: $attachment_text;
+
+ .mail-list-entry__checkbox::after {
+ display: none;
+ }
+ }
+
+
+ &.selected {
+ background: $light_blue;
+ z-index: 10; // overlay the box-shadow of the right page (z-index: 2)
+
+ &:hover {
+ background: $light_blue;
+ }
+
+ a {
+ color: $white;
+ }
+ }
+
+ &:hover {
+ background: darken($contrast, 5%);
+ }
+
+ &__checkbox {
+ margin-right: 5px;
+ display: block;
+ float: left;
+ margin: {
+ top: 8px;
+ left: 20px;
+ }
+
+ &::after {
+ content: '';
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ -moz-border-radius: 15px;
+ -webkit-border-radius: 15px;
+ border-radius: 15px;
+ background-color: $bullet-blue;
+ position: absolute;
+ left: 48px;
+ top: 13px;
+ }
+
+ & > input[type=checkbox] {
+ @include check-box;
+ }
+ }
+
+ &__item {
+ display: block;
+ color: $dark_grey;
+ padding: 8px 10px 10px 67px;
+ height: 100%;
+
+ &-from {
+ white-space: nowrap;
+ font-size: 0.8em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ }
+
+ &-date {
+ font-size: 0.7em;
+ float: right;
+ display: inline-block;
+ }
+
+ &-subject {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 85%;
+
+ &-icon {
+ color: $light_gray;
+ }
+ }
+
+ &-attachment {
+ width: 14px;
+ text-align: right;
+ display: inline-block;
+ float: right;
+ color: $light_gray;
+ }
+
+ &-tags {
+ @include tags;
+
+ // Workaround:
+ // Foundation is of the opinion that a 1.6 line height and a 0.6 rem margin-bottom
+ // for all lists is a totally good idea. Please remove when Foundation is gone
+ line-height: normal;
+ margin-bottom: 0;
+ }
+
+ &:hover, &:focus, &:active {
+ color: $dark_grey;
+ }
+ }
+}
diff --git a/web-ui/app/scss/views/_message-panel.scss b/web-ui/app/scss/views/_message-panel.scss
new file mode 100644
index 00000000..4a0a7a6b
--- /dev/null
+++ b/web-ui/app/scss/views/_message-panel.scss
@@ -0,0 +1,26 @@
+.message-panel {
+ width: 100%;
+ margin: 10px auto;
+ position: fixed;
+ z-index: 10000;
+ text-align: center;
+
+ &__growl {
+ padding: 5px 60px;
+
+ &--success {
+ background: $warning;
+ color: darken($warning, 50%);
+ border: 1px solid darken($warning, 10%);
+ @include box-shadow(1px 1px 3px darken($warning, 60%));
+ }
+
+ &--error {
+ font-weight: bold;
+ color: white;
+ background: $error;
+ border: 1px solid darken($error, 10%);
+ @include box-shadow(1px 1px 3px darken($error, 60%));
+ }
+ }
+}
diff --git a/web-ui/app/scss/views/_navigation.scss b/web-ui/app/scss/views/_navigation.scss
new file mode 100644
index 00000000..2c33a791
--- /dev/null
+++ b/web-ui/app/scss/views/_navigation.scss
@@ -0,0 +1,589 @@
+#logo {
+ color: $white;
+}
+
+#logout {
+ color: $white;
+ cursor: pointer;
+}
+
+#user-settings-box {
+ position: fixed;
+ z-index: 10;
+
+ & > div {
+ position: fixed;
+ left: 70px;
+ bottom: 0px;
+ z-index: 1;
+ padding: 10px 16px 10px 18px;
+ background-color: rgba($dark_slate_gray, 0.9);
+ min-width: 230px;
+
+ &.extra-bottom-space {
+ bottom: 33px;
+ }
+
+ header {
+ border-bottom: 1px solid white;
+ margin-bottom: 10px;
+ }
+
+ #user-settings-close {
+ float: right;
+ }
+
+ h1, i {
+ font-size: 1.2em;
+ color: white;
+ line-height: 1.2em;
+ }
+
+ h2 {
+ font-size: 1.1em;
+ color: white;
+ line-height: 1.1em;
+ display: inline;
+ margin-left: 5px;
+ }
+
+ i.fa-user {
+ margin-right: 10px;
+ float: left;
+ }
+
+ i.fa-close {
+ margin-left: 10px;
+ float: right;
+ cursor: pointer;
+ }
+
+ p {
+ font-size: 1.1em;
+ color: $light_orange;
+ }
+ }
+}
+
+@keyframes hideshow {
+ 0% {
+ fill: lighten($logo_color, 30);
+ }
+
+ 25% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+.logo-part-animation-off {
+ animation: none;
+}
+
+.logo-part-animation-on {
+ animation: hideshow 0.6s ease infinite;
+ opacity: 1;
+
+ &:nth-child(2) {
+ opacity: 0;
+ animation-delay: 0.1s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(4) {
+ animation-delay: 0.3s;
+ }
+
+ &:nth-child(5) {
+ animation-delay: 0.4s;
+ }
+
+ &:nth-child(6) {
+ animation-delay: 0.5s;
+ }
+}
+
+.arrow-box:before {
+ right: 100%;
+ top: 65%;
+ border: 20px solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border-right-color: rgba($dark_slate_gray, 0.9);
+ margin-top: -20px;
+}
+
+.side-nav-toggle, .side-nav-toggle-icon {
+ color: white;
+ cursor: pointer;
+
+ &:hover, &:focus {
+ color: white;
+ }
+
+ background: $navigation_background;
+
+ &.logout {
+ color: $action_buttons;
+ }
+}
+
+.side-nav-toggle-icon {
+ padding: 6px 0px 8px 19px;
+ display: block;
+ left: 0;
+ top: 0;
+ position: relative;
+
+ .fa-navicon {
+ font-size: 24px;
+ &:before {
+ margin-left: -5px;
+ }
+ }
+}
+
+.left-off-canvas-logo {
+ svg {
+ width: 162px;
+ height: 56px;
+ padding-left: 6px;
+ padding-top: 2px;
+
+ path, polygon, rect {
+ fill: $logo_color;
+ }
+ }
+}
+
+.collapsed-nav {
+ width: 50px;
+ position: absolute;
+ height: 100vh;
+ background: $navigation_background;
+
+ ul.shortcuts {
+ li {
+ position: relative;
+ margin-bottom: 5px;
+ opacity: 0.8;
+
+ &.selected {
+ background: $contrast;
+ opacity: 1;
+ cursor: default;
+
+ a {
+ color: $navigation_background;
+ }
+ }
+
+ @include searching(6px, 26px, $medium_dark_grey, 0.9em);
+
+ a {
+ display: block;
+ position: relative;
+ font-size: 1.4em;
+ padding: 5px;
+ color: white;
+ text-align: center;
+
+ &:hover {
+ background: darken($contrast, 10%);
+ color: $navigation_background;
+
+ @include btn-transition;
+
+ &.logout {
+ color: $black;
+ background: $action_buttons;
+ }
+ }
+
+ &[title]:hover:after {
+ content: attr(title);
+
+ @include tooltip;
+ }
+ }
+ }
+ }
+
+ #custom-tags-shortcuts {
+ li {
+ border-top: 1px solid $lighter_gray;
+ }
+ }
+
+ div.shortcut-label {
+ font-size: xx-small;
+ text-transform: uppercase;
+ text-align: center;
+ }
+}
+
+.move-right {
+ ul.shortcuts {
+ li {
+ display: none;
+ }
+ }
+}
+
+.left-off-canvas-menu {
+ width: 222px;
+ -webkit-backface-visibility: hidden;
+ box-sizing: content-box;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ position: absolute;
+ overflow-y: auto;
+}
+
+.left-off-canvas-menu * {
+ -webkit-backface-visibility: hidden;
+}
+
+.off-canvas-wrap {
+ -webkit-backface-visibility: hidden;
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.off-canvas-wrap.move-right, .off-canvas-wrap.move-left {
+ min-height: 100%;
+ -webkit-overflow-scrolling: touch;
+}
+
+.inner-wrap {
+ -webkit-backface-visibility: hidden;
+ width: 100%;
+}
+
+.inner-wrap:before, .inner-wrap:after {
+ content: " ";
+ display: table;
+}
+
+.inner-wrap:after {
+ clear: both;
+}
+
+.off-canvas-wrap.content {
+ -webkit-ransition: -webkit-transform 500ms ease;
+ -moz-transition: -moz-transform 500ms ease;
+ -ms-transition: -ms-transform 500ms ease;
+ -o-transition: -o-transform 500ms ease;
+ transition: transform 500ms ease;
+
+ &.move-right {
+ -webkit-transform: translate3d(10rem, 0, 0);
+ -moz-transform: translate3d(10rem, 0, 0);
+ -ms-transform: translate3d(10rem, 0, 0);
+ -o-transform: translate3d(10rem, 0, 0);
+ transform: translate3d(10rem, 0, 0);
+
+ #user-settings-box > div {
+ left: 20px;
+ }
+ }
+}
+
+.move-right .exit-off-canvas {
+ -webkit-backface-visibility: hidden;
+ transition: background 300ms ease;
+ cursor: pointer;
+ display: block;
+ position: absolute;
+ background: rgba(255, 255, 255, 0.2);
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+@media only screen and (min-width: 40.063em) {
+ .move-right .exit-off-canvas:hover {
+ background: rgba(255, 255, 255, 0.05);
+ }
+}
+
+.off-canvas-wrap.move-right.menu {
+ position: absolute;
+}
+
+.off-canvas-wrap.content {
+ left: 50px;
+ padding-right: 50px;
+}
+
+.offcanvas-overlap .left-off-canvas-menu, .offcanvas-overlap .right-off-canvas-menu {
+ -ms-transform: none;
+ -webkit-transform: none;
+ -moz-transform: none;
+ -o-transform: none;
+ transform: none;
+}
+
+.offcanvas-overlap .exit-offcanvas-menu {
+ -webkit-backface-visibility: hidden;
+ transition: background 300ms ease;
+ cursor: pointer;
+ display: block;
+ position: absolute;
+ background: rgba(255, 255, 255, 0.2);
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+div.side-nav-bottom {
+ width: 100%;
+ position: fixed;
+ bottom: 20px;
+ background-color: $navigation_background;
+
+ .version {
+ padding-left: 55px;
+ padding-bottom: 3px;
+ }
+}
+
+#left-pane nav {
+ border-right: 1px solid lighten($navigation_background, 10%);
+
+ ul#default-tag-list, #custom-tag-list {
+ li {
+ transition: background-color 150ms ease-out;
+ padding: 2px 10px;
+ cursor: pointer;
+
+ &:hover {
+ background: $light_gray;
+ color: $navigation_background;
+ }
+
+ &.selected {
+ font-weight: bold;
+ background: $contrast;
+ color: $navigation_background;
+ }
+ }
+ }
+
+ ul#default-tag-list {
+
+ span.tag-label {
+ padding-left: 2px;
+ }
+
+ li {
+ padding: 5px 10px 5px 18px;
+ position: relative;
+
+ @include searching(4px, 19px, $dark_grey, 0.7em);
+
+ &:before {
+ font-size: 1.5em;
+ font-family: "FontAwesome";
+ margin-right: 16px;
+ font-weight: normal;
+ position: relative;
+ top: 2px;
+ margin-left: -3px;
+ }
+
+ &:after {
+ padding-left: 10px;
+ }
+
+ &:nth-child(1) {
+ &:before {
+ content: "\f01c";
+ }
+ }
+
+ &:nth-child(2) {
+ &:before {
+ font-family: "icomoon";
+ content: "\e900";
+ margin-left: -5px;
+ }
+ }
+
+ &:nth-child(3) {
+ &:before {
+ content: "\f040";
+ }
+ }
+
+ &:nth-child(4) {
+ &:before {
+ content: "\f014";
+ }
+ }
+
+ &:nth-child(5) {
+ &:before {
+ content: "\f187";
+ margin-left: -5px;
+ }
+ }
+ }
+ }
+
+ ul#custom-tag-list {
+ visibility: hidden;
+ opacity: 0;
+ transition-duration: 500ms;
+ height: 100%;
+ max-height: 220px;
+ overflow: auto;
+ background-color: lighten($navigation_background, 1);
+
+ li {
+ white-space: nowrap;
+ overflow: hidden;
+ font-size: 0.8em;
+ padding: 5px 10px 5px 15px;
+
+ &.custom-tag {
+ text-overflow: ellipsis;
+ }
+
+ span.tag-label {
+ padding: 5px 20px 5px 38px;
+ }
+ }
+
+ .unread-count, .total-count {
+ padding: 1px 4px;
+ position: relative;
+ }
+
+ }
+
+ ul#custom-tag-list.expanded {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ div.tags-icon {
+ border-top: 1px solid white;
+ padding-top: 25px;
+ margin-bottom: 20px;
+
+ i {
+ font-size: 1.5em;
+ font-family: "FontAwesome";
+ margin-right: 13px;
+ font-weight: normal;
+ position: relative;
+ top: 2px;
+ left: 16px;
+ }
+
+ span.tag-label {
+ font-size: 0.9rem;
+ padding-left: 16px;
+ margin-bottom: 10px;
+ }
+ }
+
+ ul#logout, ul#feedback, ul#user-settings-icon {
+ margin-bottom: 0;
+
+ li {
+ background-color: $navigation_background;
+ padding: 5px 10px;
+ position: relative;
+
+ @include searching(4px, 19px, $dark_grey, 0.7em);
+
+ &:hover {
+ color: $navigation_background;
+ }
+
+ div {
+ padding-left: 7px;
+
+ &:before {
+ font-size: 1.5em;
+ font-family: "FontAwesome";
+ margin-right: 13px;
+ font-weight: normal;
+ position: relative;
+ top: 2px;
+ }
+ }
+ }
+ }
+
+ ul {
+ &#logout li {
+ color: $action_buttons;
+
+ &:hover {
+ background-color: $action_buttons;
+ }
+ }
+
+ &#user-settings-icon {
+ li {
+ color: white;
+
+ &:hover {
+ background-color: white;
+ }
+ }
+ }
+
+ &#feedback {
+ margin-bottom: 0;
+
+ li {
+ color: $light_orange;
+
+ &:hover {
+ background-color: $light_orange;
+ }
+ }
+ }
+ }
+
+ h3 {
+ color: white;
+ text-transform: uppercase;
+ font-size: 0.6em;
+ padding: 5px;
+ font-weight: 600;
+ margin: 0 10px;
+ border-bottom: 1px dotted lighten($navigation_background, 10%);
+ }
+}
+
+.unread-count {
+ @extend .mail-count;
+
+ background: $secondary_callout;
+}
+
+.total-count {
+ @extend .mail-count;
+
+ background: $medium_light_grey;
+}
diff --git a/web-ui/app/scss/views/_no-mails-available.scss b/web-ui/app/scss/views/_no-mails-available.scss
new file mode 100644
index 00000000..bf5d256a
--- /dev/null
+++ b/web-ui/app/scss/views/_no-mails-available.scss
@@ -0,0 +1,3 @@
+.no-mails-available-pane {
+ @extend .no-content-placeholder;
+}
diff --git a/web-ui/app/scss/views/_no-message-selected.scss b/web-ui/app/scss/views/_no-message-selected.scss
new file mode 100644
index 00000000..0e367bf2
--- /dev/null
+++ b/web-ui/app/scss/views/_no-message-selected.scss
@@ -0,0 +1,14 @@
+.no-message-selected-pane {
+ background: $contrast;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ &__text {
+ @extend .no-content-placeholder;
+
+ margin-bottom: 40px; // aligns label with "no results for XYZ"
+ }
+}
diff --git a/web-ui/app/scss/views/_read-view.scss b/web-ui/app/scss/views/_read-view.scss
new file mode 100644
index 00000000..f69d51a5
--- /dev/null
+++ b/web-ui/app/scss/views/_read-view.scss
@@ -0,0 +1,165 @@
+.mail-read-view {
+ $component-vertical-spacing: 10px;
+ $view-top-spacing: 3px;
+
+ // NB! Setting overflow: hidden on an element causes
+ // a new float context to be created, so elements that
+ // are floated inside an element that has overflow: hidden
+ // applied are cleared.
+ overflow: hidden;
+
+ hr {
+ margin: 0;
+ }
+
+ &__header {
+ @include scut-clearfix;
+
+ font-size: 0.9em;
+ margin: 0;
+ margin: $view-top-spacing 0 $component-vertical-spacing 0;
+
+ &-recipients {
+ display: inline;
+ margin-bottom: 5px;
+ line-height: 1.5em;
+
+ &-separator {
+ margin: 0 10px;
+ }
+
+ &--highlight-sender {
+ font-weight: bold;
+ }
+ }
+
+ &-date {
+ display: inline;
+ float: right;
+ }
+
+ &-subject {
+ display: inline;
+ float: left;
+ max-width: 80%;
+ }
+
+ &-actions {
+ display: inline;
+ float: right;
+ max-width: 20%;
+ background: $white;
+ white-space: nowrap;
+ margin-top: $component-vertical-spacing;
+
+ &-button {
+ color: $medium_light_grey;
+ background-color: inherit;
+ display: inline;
+ border: 1px solid $lighter_gray;
+ line-height: 2em;
+
+ margin-bottom: 0;
+
+ i {
+ // workaround: remove padding and margin inserted by font-awesome
+ margin: 0;
+ padding: 0;
+ }
+
+ &:hover, &:active, &:focus {
+ @include btn-transition;
+
+ background: darken($contrast, 5%);
+ color: inherit;
+ }
+
+ &--reply {
+ padding: 0 20px;
+ margin-right: -4px; // force buttons together
+
+ }
+
+ &--more {
+ padding: 0 5px;
+ }
+ }
+
+ &-dropdown {
+ $container-right-padding: 10px;
+
+ background: inherit;
+ position: absolute;
+ border: 1px solid $lighter_gray;
+ right: $container-right-padding;
+
+ &-entry {
+ box-sizing: border-box;
+ background: inherit;
+ padding: 5px 10px;
+ display: block;
+ border-bottom: 1px solid $lighter_gray;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ cursor: pointer;
+ background: $contrast;
+ }
+ }
+ }
+ }
+
+ &-tags {
+ @include tags-editable;
+
+ clear: both;
+ margin: 0 0 10px;
+ }
+ }
+
+ &__body {
+ margin: $component-vertical-spacing 0;
+ width: 100%;
+ border: none;
+ }
+
+ &__attachments {
+ margin: $component-vertical-spacing 0;
+
+ &-header {
+ font-weight: bold;
+ }
+
+ &-item {
+ display: block;
+ margin-bottom: 8px;
+ padding: 5px;
+ border: 1px solid $border_light_grey;
+ border-radius: 2px;
+ background-color: $background_light_grey;
+
+ &-label {
+ color: $attachment_text;
+ text-decoration: none;
+
+ &:hover, &:focus {
+ i.download-icon {
+ color: lighten($attachment_icon, 15);
+ }
+
+ color: $attachment_icon;
+ outline: none;
+ }
+ }
+
+ &-download {
+ color: #a2a2a2;
+ float: right;
+ margin-top: 5px;
+ }
+ }
+ }
+}
diff --git a/web-ui/app/scss/views/_security-labels.scss b/web-ui/app/scss/views/_security-labels.scss
new file mode 100644
index 00000000..ac966ded
--- /dev/null
+++ b/web-ui/app/scss/views/_security-labels.scss
@@ -0,0 +1,67 @@
+.security-status {
+ margin: 0 0 5px;
+
+ &__label {
+ display: inline-block;
+ padding: 2px 6px;
+ white-space: nowrap;
+ background: $success;
+ color: $white;
+ border-radius: 12px;
+
+ &:before {
+ font-family: FontAwesome;
+ }
+
+ &--encrypted {
+ &:before {
+ content: "\f023";
+ }
+
+ &--with-error {
+ background: $attention;
+ &:before {
+ content: "\f023 \f057";
+ }
+ }
+ }
+
+ &--not-encrypted {
+ background: $attention;
+
+ &:before {
+ content: "\f09c";
+ }
+ }
+
+ &--signed {
+ &:before {
+ content: "\f00c";
+ }
+
+ &--revoked, &--expired {
+ background: $attention;
+
+ &:before {
+ content: "\f05e";
+ }
+ }
+
+ &--not-trusted {
+ background: $error;
+
+ &:before {
+ content: "\f05e";
+ }
+ }
+ }
+
+ &--not-signed {
+ background: $attention;
+
+ &:before {
+ content: "\f05e";
+ }
+ }
+ }
+}
diff --git a/web-ui/app/templates/compose/attachment_item.hbs b/web-ui/app/templates/compose/attachment_item.hbs
new file mode 100644
index 00000000..7a64f6f5
--- /dev/null
+++ b/web-ui/app/templates/compose/attachment_item.hbs
@@ -0,0 +1,4 @@
+<li data-ident="{{ this.ident }}" class="compose-view__attachments-list-item">
+ <a class="compose-view__attachments-list-item-label" href="/attachment/{{ this.ident }}?encoding={{ this.encoding }}&filename={{ this.name }}">{{ this.name }} <span class="attachment-size">({{ formatSize this.size}})</span></a>
+ {{#if removable}}<i class="fa fa-close remove-icon compose-view__attachments-list-item-icon"></i>{{/if}}
+</li>
diff --git a/web-ui/app/templates/compose/attachment_upload_item.hbs b/web-ui/app/templates/compose/attachment_upload_item.hbs
new file mode 100644
index 00000000..eb6c4ba6
--- /dev/null
+++ b/web-ui/app/templates/compose/attachment_upload_item.hbs
@@ -0,0 +1,5 @@
+<li class="compose-view__attachments-list-item">
+ <div id="attachment-upload-item-progress" class="compose-view__attachments-list-item-progress"><div class="compose-view__attachments-list-item-progress-bar"></div></div>
+ <a class="compose-view__attachments-list-item-label">{{ this.name }} <span class="attachment-size">({{ formatSize this.size}})</span></a>
+ <i id="attachment-upload-item-abort" class="fa fa-close remove-icon compose-view__attachments-list-item-icon"></i>
+</li>
diff --git a/web-ui/app/templates/compose/attachments_list.hbs b/web-ui/app/templates/compose/attachments_list.hbs
new file mode 100644
index 00000000..6f34df9e
--- /dev/null
+++ b/web-ui/app/templates/compose/attachments_list.hbs
@@ -0,0 +1,14 @@
+<div id="attachment-list" class="buttons-group columns compose-view__attachments">
+ <input id="fileupload" type="file" name="attachment" hidden>
+
+ <!-- The container for the uploaded files -->
+ <div class="compose-view__attachments-wrapper">
+ <ul id="attachment-list-item" class="compose-view__attachments-list">
+ {{#each attachments }}
+ {{> attachment_item this }}
+ {{/each }}
+ </ul>
+ <ul id="attachment-upload-item" class="compose-view__attachments-list compose-view__attachments-list--upload"></ul>
+ </div>
+
+</div>
diff --git a/web-ui/app/templates/compose/compose_box.hbs b/web-ui/app/templates/compose/compose_box.hbs
new file mode 100644
index 00000000..fcfbeaaf
--- /dev/null
+++ b/web-ui/app/templates/compose/compose_box.hbs
@@ -0,0 +1,32 @@
+<button class="close-mail-button">
+ <i class="fa fa-times"></i>
+</button>
+
+<div class="compose-view">
+
+ {{> recipients }}
+ <div class="clearfix">
+ <a id="to-trigger" class="hide">{{t 'to'}}</a>
+ <a id="ccs-trigger" class="hide">{{t 'cc'}}</a>
+ <a id="bccs-trigger" class="hide">{{t 'bcc'}}</a>
+ </div>
+
+ <div class="floatlabel">
+ <label class="floatlabel" for="subject">{{t 'subject'}}</label>
+ <input class="floatlabel" name="subject" type="text" id="subject" value="{{subject}}" placeholder="{{t 'subject'}}" tabindex="4"/>
+ </div>
+ <div class="floatlabel">
+ <label class="floatlabel" for="body">{{t 'body'}}</label>
+ <textarea class="floatlabel" name="body" id="text-box" placeholder="{{t 'body'}}" tabindex="5">{{body}}</textarea>
+ </div>
+
+ {{> attachments_list }}
+
+ <div class="buttons-group columns compose-view__buttons">
+ <button id="send-button" tabindex="6"><i class="fa fa-send"></i></button>
+ <div class="compose-view__buttons-attachment"><span id="attachment-button" tabindex="6"></span></div>
+ <button id="trash-button" tabindex="7">{{t 'trash-button'}}<i class="fa fa-trash-o"></i></button>
+ <div id="draft-save-status"></div>
+ </div>
+
+</div> <!-- ./compose-view -->
diff --git a/web-ui/app/templates/compose/feedback_box.hbs b/web-ui/app/templates/compose/feedback_box.hbs
new file mode 100644
index 00000000..346a6192
--- /dev/null
+++ b/web-ui/app/templates/compose/feedback_box.hbs
@@ -0,0 +1,18 @@
+<button class="close-mail-button">
+ <i class="fa fa-times"></i>
+</button>
+
+<div class="compose-view">
+ <div class="floatlabel">
+ <span id="feedback-subject">Feedback</span>
+ </div>
+
+ <div class="floatlabel">
+ <label class="floatlabel" for="text-box">Body</label>
+ <textarea class="floatlabel" name="body" id="text-box" placeholder="{{t 'feedback-placeholder'}}" tabindex="2">{{body}}</textarea>
+ </div>
+
+ <div class="buttons-group columns">
+ <button id="send-button" tabindex="6">{{t 'Submit Feedback'}} </button>
+ </div>
+</div> <!-- ./compose-view -->
diff --git a/web-ui/app/templates/compose/fixed_recipient.hbs b/web-ui/app/templates/compose/fixed_recipient.hbs
new file mode 100644
index 00000000..8b01717c
--- /dev/null
+++ b/web-ui/app/templates/compose/fixed_recipient.hbs
@@ -0,0 +1,8 @@
+<div class="fixed-recipient">
+ <span class="recipient-area">
+ <div class="recipient-value">
+ <span>{{ address }}</span> <a class="recipient-del" href="#" data-label="{{t 'click-to-remove'}}"/>
+ </div>
+ </span>
+ <input type="hidden" value="{{ address }}" name="{{ name }}" />
+</div>
diff --git a/web-ui/app/templates/compose/inline_box.hbs b/web-ui/app/templates/compose/inline_box.hbs
new file mode 100644
index 00000000..c9c114ec
--- /dev/null
+++ b/web-ui/app/templates/compose/inline_box.hbs
@@ -0,0 +1,20 @@
+<div id="subject-container">
+ <h4 id="reply-subject">{{subject}}</h4>
+ <input type="text" value="{{subject}}" style="display: none"/>
+</div>
+<textarea id="text-box" placeholder="{{t 'body'}}" tabindex=1>{{body}}</textarea>
+
+<a id="all-recipients" tabindex=2>
+ <strong>{{t 'to'}}:</strong> {{formatRecipients recipients}}
+</a>
+
+{{> recipients }}
+
+{{> attachments_list }}
+
+<div class="buttons-group columns compose-view__buttons">
+ <button id="send-button" tabindex=6><i class="fa fa-send"></i></button>
+ <div class="compose-view__buttons-attachment"><span id="attachment-button" tabindex="6"></span></div>
+ <button id="trash-button" tabindex=7>{{t 'trash-button'}}<i class="fa fa-trash-o"></i></button>
+ <div id="draft-save-status"></div>
+</div>
diff --git a/web-ui/app/templates/compose/no_mails_available.hbs b/web-ui/app/templates/compose/no_mails_available.hbs
new file mode 100644
index 00000000..c61152a4
--- /dev/null
+++ b/web-ui/app/templates/compose/no_mails_available.hbs
@@ -0,0 +1,7 @@
+<div class="no-mails-available-pane">
+ {{#if forSearch }}
+ {{t 'no-results-for'}}: '{{ forSearch }}'.
+ {{else}}
+ {{t 'no-emails-in'}} '{{t tag}}'.
+ {{/if}}
+</div>
diff --git a/web-ui/app/templates/compose/no_message_selected.hbs b/web-ui/app/templates/compose/no_message_selected.hbs
new file mode 100644
index 00000000..0b9beaf8
--- /dev/null
+++ b/web-ui/app/templates/compose/no_message_selected.hbs
@@ -0,0 +1,3 @@
+<div class="no-message-selected-pane">
+ <div class="no-message-selected-pane__text">{{t 'nothing-selected'}}.</div>
+</div>
diff --git a/web-ui/app/templates/compose/recipient_input.hbs b/web-ui/app/templates/compose/recipient_input.hbs
new file mode 100644
index 00000000..9416f11f
--- /dev/null
+++ b/web-ui/app/templates/compose/recipient_input.hbs
@@ -0,0 +1 @@
+<input type="text" />
diff --git a/web-ui/app/templates/compose/recipients.hbs b/web-ui/app/templates/compose/recipients.hbs
new file mode 100644
index 00000000..43aced1c
--- /dev/null
+++ b/web-ui/app/templates/compose/recipients.hbs
@@ -0,0 +1,33 @@
+<div id="recipients-fields" style="display:none">
+ <div id='recipients-to-area' class="recipients-area input-container columns large-12 no-padding">
+ <input class="recipients-navigation-handler"/>
+ <div class='compose-column-label'>
+ <label for="recipients-to-box" class="recipients-label column large-1">{{t 'to'}}: </label>
+ </div>
+ <div class='recipients-list compose-column-recipients'>
+ <input id='recipients-to-box' class="recipients-input" type="text" tabindex="1"/></div>
+ </div>
+ <label id="cc-bcc-collapse" class="collapse fa-angle-down" for="hide-and-show"></label>
+ <input id="hide-and-show" type="checkbox">
+ <div id="cc-and-bcc">
+ <div id="recipients-cc-area" class="recipients-area input-container columns large-12 no-padding">
+ <input class="recipients-navigation-handler"/>
+ <div class='compose-column-label'>
+ <label for="recipients-cc-box" class="recipients-label column large-1">{{t 'cc'}}: </label>
+ </div>
+ <div class='recipients-list compose-column-recipients'>
+ <input id='recipients-cc-box' class="recipients-input" type="text" tabindex="2"/>
+ </div>
+ </div>
+
+ <div id="recipients-bcc-area" class="recipients-area input-container columns large-12 no-padding">
+ <input class="recipients-navigation-handler"/>
+ <div class='compose-column-label'>
+ <label for ="recipients-bcc-box" class="recipients-label column large-1">{{t 'bcc'}}: </label>
+ </div>
+ <div class='recipients-list compose-column-recipients'>
+ <input id='recipients-bcc-box' class="recipients-input" type="text" tabindex="3"/>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/web-ui/app/templates/compose/reply_section.hbs b/web-ui/app/templates/compose/reply_section.hbs
new file mode 100644
index 00000000..45203d87
--- /dev/null
+++ b/web-ui/app/templates/compose/reply_section.hbs
@@ -0,0 +1,6 @@
+<div class="reply-container columns small-12 large-12">
+ <button id="reply-button" class="column small-12 large-4">{{t 'reply'}} <i class="fa fa-reply"></i></button>
+ <button id="reply-all-button" class="column small-12 large-4">{{t 'reply-to-all'}} <i class="fa fa-reply-all"></i></button>
+ <button id="forward-button" class="column small-12 large-4">{{t 'forward'}} <i class="fa fa-mail-forward"></i></button>
+ <div id="reply-box" style="display:none"></div>
+</div>
diff --git a/web-ui/app/templates/compose/upload_attachment_failed.hbs b/web-ui/app/templates/compose/upload_attachment_failed.hbs
new file mode 100644
index 00000000..dbb1437b
--- /dev/null
+++ b/web-ui/app/templates/compose/upload_attachment_failed.hbs
@@ -0,0 +1,6 @@
+<div id="upload-error" class="compose-view__attachments-error">
+ <i id="upload-error-close" class="fa fa-close compose-view__attachments-error-close"></i>
+ <span id="upload-error-message" class="compose-view__attachments-error-message">Upload failed. This file exceeds the 1MB limit.</span>
+ <a href="#" id="upload-file-button">Choose another file</a>
+ <a href="#" id="dismiss-button">Dismiss</a>
+</div>
diff --git a/web-ui/app/templates/feedback/feedback_trigger.hbs b/web-ui/app/templates/feedback/feedback_trigger.hbs
new file mode 100644
index 00000000..7f3f8ef1
--- /dev/null
+++ b/web-ui/app/templates/feedback/feedback_trigger.hbs
@@ -0,0 +1,8 @@
+<ul id="feedback">
+ <a title="Feedback" href="#">
+ <li>
+ <div class="fa fa-exclamation-circle"></div>
+ <i class="shortcut-label"></i> Feedback
+ </li>
+ </a>
+</ul>
diff --git a/web-ui/app/templates/mail_actions/actions_box.hbs b/web-ui/app/templates/mail_actions/actions_box.hbs
new file mode 100644
index 00000000..68a8d0bf
--- /dev/null
+++ b/web-ui/app/templates/mail_actions/actions_box.hbs
@@ -0,0 +1,7 @@
+<li><input type="checkbox" id="toggle-check-all-emails"/></li>
+<li><input type="button" id="mark-selected-as-read" value="{{t 'mark-as-read'}}" disabled="disabled"/></li>
+<li><input type="button" id="mark-selected-as-unread" value="{{t 'mark-as-unread'}}" disabled="disabled"/></li>
+<li><input type="button" id="delete-selected" value="{{t 'delete'}}" disabled="disabled"/></li>
+<li><input type="button" id="archive-selected" value="{{t 'archive'}}" disabled="disabled"/></li>
+<li id="pagination-trigger" class="right"></li>
+<li id="refresh-trigger" class="right"></li>
diff --git a/web-ui/app/templates/mail_actions/compose_trigger.hbs b/web-ui/app/templates/mail_actions/compose_trigger.hbs
new file mode 100644
index 00000000..06f05fca
--- /dev/null
+++ b/web-ui/app/templates/mail_actions/compose_trigger.hbs
@@ -0,0 +1,3 @@
+<button id="compose-mails-trigger">
+ {{t 'compose' }}
+</button>
diff --git a/web-ui/app/templates/mail_actions/pagination_trigger.hbs b/web-ui/app/templates/mail_actions/pagination_trigger.hbs
new file mode 100644
index 00000000..cbd8a089
--- /dev/null
+++ b/web-ui/app/templates/mail_actions/pagination_trigger.hbs
@@ -0,0 +1,3 @@
+<span id="left-arrow"><i class="fa fa-angle-left"></i></span>
+<span id="current-page">{{ currentPage }}</span>
+<span id="right-arrow"><i class="fa fa-angle-right"></i></span>
diff --git a/web-ui/app/templates/mail_actions/refresh_trigger.hbs b/web-ui/app/templates/mail_actions/refresh_trigger.hbs
new file mode 100644
index 00000000..dffc7090
--- /dev/null
+++ b/web-ui/app/templates/mail_actions/refresh_trigger.hbs
@@ -0,0 +1,3 @@
+<div id="refresh-mails-trigger">
+ <i class="fa fa-refresh" data-label="{{t 'refresh'}}"></i>
+</div>
diff --git a/web-ui/app/templates/mail_actions/trash_actions_box.hbs b/web-ui/app/templates/mail_actions/trash_actions_box.hbs
new file mode 100644
index 00000000..4e0ec332
--- /dev/null
+++ b/web-ui/app/templates/mail_actions/trash_actions_box.hbs
@@ -0,0 +1,5 @@
+<li><input type="checkbox" id="toggle-check-all-emails"/></li>
+<li><input type="button" id="delete-selected" value="{{t 'delete-permanently'}}" disabled="disabled"/></li>
+<li><input type="button" id="recover-selected" value="{{t 'move-to-inbox'}}" disabled="disabled"/></li>
+<li id="pagination-trigger" class="right"></li>
+<li id="refresh-trigger" class="right"></li>
diff --git a/web-ui/app/templates/mails/draft.hbs b/web-ui/app/templates/mails/draft.hbs
new file mode 100644
index 00000000..808ce3ff
--- /dev/null
+++ b/web-ui/app/templates/mails/draft.hbs
@@ -0,0 +1,41 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{t 'to'}}:
+ {{#if header.to }}
+ {{ header.to }}
+ {{else}}
+ {{t 'no-recipient'}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">
+ <i class="mail-list-entry__item-subject-icon fa fa-pencil"></i>
+ {{#if header.subject }}
+ {{header.subject}}
+ {{else}}
+ {{t 'no-subject'}}
+ {{/if}}
+ </div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
+
+
+
+
diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs
new file mode 100644
index 00000000..40bfd4a2
--- /dev/null
+++ b/web-ui/app/templates/mails/full_view.hbs
@@ -0,0 +1,83 @@
+<button class="close-mail-button">
+ <i class="fa fa-times"></i>
+</button>
+
+<div id="fullView-{{ ident }}" class="mail-read-view {{statuses}}">
+ <header class="mail-read-view__header row">
+
+ <!-- TODO -->
+ <div class="column large-12 no-padding security-status">
+ {{#if signatureStatus}}
+ <span class="security-status__label {{ signatureStatus.cssClass }}">
+ {{t signatureStatus.label }}
+ </span>
+ {{/if}}
+ {{#if encryptionStatus}}
+ <span class="security-status__label {{ encryptionStatus.cssClass }}">
+ {{t encryptionStatus.label }}
+ </span>
+ {{/if}}
+ </div>
+
+ <div class="mail-read-view__header-recipients">
+ <span class="mail-read-view__header-recipients--highlight-sender">
+ {{#if header.from }}
+ {{ header.from }}
+ {{else}}
+ {{t 'you'}}
+ {{/if}}
+ </span>
+ <i class="fa fa-long-arrow-right"></i>
+ {{{formatRecipients header}}}
+ </div>
+
+ <div class="mail-read-view__header-date">
+ {{ formatDate header.date }}
+ </div>
+
+ <hr>
+
+ <div class="mail-read-view__header-subject">
+ <h3>{{ header.subject }}</h3>
+ </div>
+
+ <nav id="mail-actions" class="mail-read-view__header-actions"></nav>
+
+ <ul class="mail-read-view__header-tags">
+ <li class="mail-read-view__header-tags-label">
+ <i class="fa fa-tags"></i>
+ </li>
+
+ {{#each tags }}
+ <li class="mail-read-view__header-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+
+ <li class="mail-read-view__header-tags-name-input">
+ <input type="text" id="new-tag-input" placeholder="{{t 'add-tag-placeholder'}}"
+ />
+ </li>
+
+ <li class="mail-read-view__header-tags-new-button">
+ <button id="new-tag-button" class="no-style"><i class="fa fa-plus"></i></button>
+ </li>
+ </ul>
+ </header>
+
+ <iframe class="mail-read-view__body" id="read-sandbox" sandbox="allow-popups allow-scripts" src="sandbox/sandbox.html" scrolling="no"></iframe>
+
+ {{#if attachments}}
+ <hr>
+
+ <div id="attachmentsArea" class="mail-read-view__attachments">
+ <p class="mail-read-view__attachments-header"><i class="fa fa-paperclip"></i> {{ attachments.length }} attachment(s):</p>
+ <ul>
+ {{#each attachments }}
+ <li class="mail-read-view__attachments-item">
+ <a class="mail-read-view__attachments-item-label" href="/attachment/{{ this.ident }}?content_type={{ this.content-type }}&encoding={{ this.encoding }}&filename={{ this.name }}">{{ this.name }} ({{ formatSize this.size}})
+ <i class="fa fa-arrow-down mail-read-view__attachments-item-download"></i></a>
+ </li>
+ {{/each }}
+ </ul>
+ </div>
+ {{/if}}
+</div>
diff --git a/web-ui/app/templates/mails/mail_actions.hbs b/web-ui/app/templates/mails/mail_actions.hbs
new file mode 100644
index 00000000..0adfe853
--- /dev/null
+++ b/web-ui/app/templates/mails/mail_actions.hbs
@@ -0,0 +1,6 @@
+<button id="reply-button-top" class="mail-read-view__header-actions-button mail-read-view__header-actions-button--reply"><i class="fa fa-reply"></i></button>
+<button id="view-more-actions" class="mail-read-view__header-actions-button mail-read-view__header-actions-button--more"><i class="fa fa-caret-down"></i></button>
+<ul id="more-actions" class="mail-read-view__header-actions-dropdown">
+ <li id="reply-all-button-top" class="mail-read-view__header-actions-dropdown-entry">{{t 'reply-to-all'}}</li>
+ <li id="delete-button-top" class="mail-read-view__header-actions-dropdown-entry">{{t 'delete-this-message'}}</li>
+</ul>
diff --git a/web-ui/app/templates/mails/sent.hbs b/web-ui/app/templates/mails/sent.hbs
new file mode 100644
index 00000000..158b20c8
--- /dev/null
+++ b/web-ui/app/templates/mails/sent.hbs
@@ -0,0 +1,36 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{t 'to'}}:
+ {{#if header.to }}
+ {{ header.to }}
+ {{else}}
+ {{t 'no-recipient'}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">
+ {{#if header.subject }}
+ {{header.subject}}
+ {{else}}
+ {{t 'no-subject'}}
+ {{/if}}
+ </div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
+
diff --git a/web-ui/app/templates/mails/single.hbs b/web-ui/app/templates/mails/single.hbs
new file mode 100644
index 00000000..aaede844
--- /dev/null
+++ b/web-ui/app/templates/mails/single.hbs
@@ -0,0 +1,28 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{#if header.from }}
+ {{ header.from }}
+ {{else}}
+ {{t "you"}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">{{ header.subject }}</div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
diff --git a/web-ui/app/templates/mails/trash.hbs b/web-ui/app/templates/mails/trash.hbs
new file mode 100644
index 00000000..f8947b15
--- /dev/null
+++ b/web-ui/app/templates/mails/trash.hbs
@@ -0,0 +1,32 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{#if header.from }}
+ {{ header.from }}
+ {{else}}
+ {{t "you"}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">
+ <i class="mail-list-entry__item-subject-icon fa fa-trash-o"></i>
+ {{ header.subject }}
+ </div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
+
diff --git a/web-ui/app/templates/page/logout.hbs b/web-ui/app/templates/page/logout.hbs
new file mode 100644
index 00000000..0cc079bc
--- /dev/null
+++ b/web-ui/app/templates/page/logout.hbs
@@ -0,0 +1,9 @@
+<ul id="logout">
+ <form id="logout-form" method="POST" action="{{ logout_url }}">
+ <input type="hidden" name="csrftoken" value="{{ csrf_token }}" />
+ <li>
+ <div class="fa fa-sign-out"></div>
+ <i class="shortcut-label"></i> {{t 'logout'}}
+ </li>
+ </form>
+</ul>
diff --git a/web-ui/app/templates/page/logout_shortcut.hbs b/web-ui/app/templates/page/logout_shortcut.hbs
new file mode 100644
index 00000000..043ab0dc
--- /dev/null
+++ b/web-ui/app/templates/page/logout_shortcut.hbs
@@ -0,0 +1,6 @@
+<li>
+ <a class="left-off-canvas-toggle logout" >
+ <i class="fa fa-sign-out"></i>
+ <div class="shortcut-label">{{t 'logout'}}</div>
+ </a>
+</li>
diff --git a/web-ui/app/templates/page/user_settings_box.hbs b/web-ui/app/templates/page/user_settings_box.hbs
new file mode 100644
index 00000000..2152b779
--- /dev/null
+++ b/web-ui/app/templates/page/user_settings_box.hbs
@@ -0,0 +1,10 @@
+<header>
+ <span id="user-settings-close"><i class="fa fa-close"></i></span>
+ <i class="fa fa-user"></i>
+ <h1>{{t 'user-account'}}</h1>
+ <i class="shortcut-label"></i>
+</header>
+<i class="fa fa-envelope-o"></i><h2>{{t 'email-address'}}</h2>
+<p>{{ account_email }}</p>
+<i class="fa fa-key"></i><h2>{{t 'public-key-fingerprint'}}</h2>
+<p>{{ formatFingerPrint fingerprint }}</p>
diff --git a/web-ui/app/templates/page/user_settings_icon.hbs b/web-ui/app/templates/page/user_settings_icon.hbs
new file mode 100644
index 00000000..8f2f9215
--- /dev/null
+++ b/web-ui/app/templates/page/user_settings_icon.hbs
@@ -0,0 +1,8 @@
+<ul id="user-settings-icon">
+ <a title="my account" href='#'>
+ <li>
+ <div class="fa fa-user"></div>
+ <i class="shortcut-label"></i> {{t 'user-account'}}
+ </li>
+ </a>
+</ul>
diff --git a/web-ui/app/templates/page/version.hbs b/web-ui/app/templates/page/version.hbs
new file mode 100644
index 00000000..5f43f78a
--- /dev/null
+++ b/web-ui/app/templates/page/version.hbs
@@ -0,0 +1,2 @@
+{{t 'version'}}: UNKNOWN_VERSION <br/>
+<span id="version-date" data-since="COMMIT_DATE"></span>
diff --git a/web-ui/app/templates/search/search_trigger.hbs b/web-ui/app/templates/search/search_trigger.hbs
new file mode 100644
index 00000000..2261d154
--- /dev/null
+++ b/web-ui/app/templates/search/search_trigger.hbs
@@ -0,0 +1,3 @@
+<form>
+ <input type="search" placeholder="{{t 'search-placeholder'}}"></input>
+</form>
diff --git a/web-ui/app/templates/tags/shortcut.hbs b/web-ui/app/templates/tags/shortcut.hbs
new file mode 100644
index 00000000..1e82d6a9
--- /dev/null
+++ b/web-ui/app/templates/tags/shortcut.hbs
@@ -0,0 +1,9 @@
+<li class="{{ selected }}">
+ <a>
+ {{#if displayBadge }}
+ <span class="{{ badgeType }}-count">{{ count }}</span>
+ {{/if}}
+ <i class="fa fa-{{ icon }}"></i>
+ <div class="shortcut-label">{{ tagName }}</div>
+ </a>
+</li>
diff --git a/web-ui/app/templates/tags/tag.hbs b/web-ui/app/templates/tags/tag.hbs
new file mode 100644
index 00000000..ca397b9a
--- /dev/null
+++ b/web-ui/app/templates/tags/tag.hbs
@@ -0,0 +1,3 @@
+<li id="tag-{{ ident }}" class="custom-tag {{ selected }}">
+ <span class="tag-label">{{> tag_inner }}</span>
+</li>
diff --git a/web-ui/app/templates/tags/tag_inner.hbs b/web-ui/app/templates/tags/tag_inner.hbs
new file mode 100644
index 00000000..2e0958cb
--- /dev/null
+++ b/web-ui/app/templates/tags/tag_inner.hbs
@@ -0,0 +1,4 @@
+{{ tagName }}
+{{#if displayBadge }}
+<span class="{{ badgeType }}-count">{{ count }}</span>
+{{/if}}
diff --git a/web-ui/app/templates/tags/tag_list.hbs b/web-ui/app/templates/tags/tag_list.hbs
new file mode 100644
index 00000000..92a73283
--- /dev/null
+++ b/web-ui/app/templates/tags/tag_list.hbs
@@ -0,0 +1,6 @@
+<ul id="default-tag-list"></ul>
+<div class="tags-icon side-nav-toggle">
+ <i class="fa fa-tags"></i>
+ <span class="tag-label">{{t 'tags.tags'}}</span>
+</div>
+<ul id="custom-tag-list"></ul>
diff --git a/web-ui/app/templates/user_alerts/message.hbs b/web-ui/app/templates/user_alerts/message.hbs
new file mode 100644
index 00000000..abba1f91
--- /dev/null
+++ b/web-ui/app/templates/user_alerts/message.hbs
@@ -0,0 +1 @@
+<span class="message-panel__growl {{ message.class }}">{{ message.content }}</span>