From ec96e998c0e0a153b0546f1ec0682c208c6876eb Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Fri, 11 Nov 2016 13:22:04 +0100 Subject: readability --- service/pixelated/bitmask_libraries/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/pixelated/bitmask_libraries/provider.py b/service/pixelated/bitmask_libraries/provider.py index 96935fbc..bc19f79e 100644 --- a/service/pixelated/bitmask_libraries/provider.py +++ b/service/pixelated/bitmask_libraries/provider.py @@ -193,7 +193,7 @@ class LeapProvider(object): fin.close() def setup_ca_bundle(self): - path = os.path.join(leap_config.leap_home, 'providers', self.server_name, 'keys', 'client') + path = os.path.dirname(self.provider_api_cert) if not os.path.isdir(path): os.makedirs(path, 0700) self._download_cert(self.provider_api_cert) -- cgit v1.2.3 From ac34bf4eeca60c967b43c9b693c56d7ae1125353 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 16 Nov 2016 15:12:25 +0100 Subject: mock up the first forms of signup --- web-ui/package.json | 22 +- web-ui/react/css/normalize.css | 461 ++++++++++++++++++++++++++ web-ui/react/css/style.css | 174 ++++++++++ web-ui/react/images/pixelated-logo-orange.svg | 29 ++ web-ui/react/images/sent_email.png | Bin 0 -> 9160 bytes web-ui/react/index.html | 18 + web-ui/react/src/index.js | 163 +++++++++ 7 files changed, 865 insertions(+), 2 deletions(-) create mode 100644 web-ui/react/css/normalize.css create mode 100644 web-ui/react/css/style.css create mode 100644 web-ui/react/images/pixelated-logo-orange.svg create mode 100644 web-ui/react/images/sent_email.png create mode 100644 web-ui/react/index.html create mode 100644 web-ui/react/src/index.js diff --git a/web-ui/package.json b/web-ui/package.json index 2a0056e4..c85889e9 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -5,20 +5,23 @@ "repository": "https://github.com/pixelated-project/pixelated-user-agent", "private": true, "devDependencies": { + "babel": "^6.5.2", + "babel-cli": "^6.18.0", "bower": "1.7.9", + "browserify": "^13.1.1", "handlebars": "4.0.5", "html-minifier": "2.1.6", "imagemin": "5.2.1", "jshint": "2.9.2", "karma": "0.13.19", "karma-chrome-launcher": "0.2.2", + "karma-coverage": "0.2.7", "karma-firefox-launcher": "0.1.7", "karma-jasmine": "0.2.2", "karma-jasmine-ajax": "0.1.13", "karma-junit-reporter": "0.2.2", "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", - "karma-coverage": "0.2.7", "minify": "2.0.9", "requirejs": "2.2.0", "watch": "0.19.1" @@ -32,7 +35,8 @@ "handlebars-watch": "node_modules/.bin/watch 'npm run handlebars' app/templates", "compass": "compass compile", "compass-watch": "compass watch", - "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass", + "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass && npm run build-react", + "build-react": "babel react/src -d react/lib && browserify react/lib/index.js >react/bundle.js", "jshint": "node_modules/jshint/bin/jshint --config=.jshintrc app test", "clean": "rm -rf .tmp/ 'dist/*' app/js/generated/hbs/* app/css/*", "buildmain": "node_modules/requirejs/bin/r.js -o config/buildoptions.js", @@ -41,5 +45,19 @@ "minify_html": "node_modules/.bin/html-minifier app/index.html --collapse-whitespace | sed 's|.*||' > dist/index.html", "minify_sandbox": "node_modules/.bin/html-minifier app/sandbox.html --collapse-whitespace | sed 's|.*||' > dist/sandbox.html", "add_git_version": "/bin/bash config/add_git_version.sh" + }, + "dependencies": { + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "immutable": "^3.8.1", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "redux": "^3.6.0" + }, + "babel": { + "presets": [ + "es2015", + "react" + ] } } diff --git a/web-ui/react/css/normalize.css b/web-ui/react/css/normalize.css new file mode 100644 index 00000000..9b77e0eb --- /dev/null +++ b/web-ui/react/css/normalize.css @@ -0,0 +1,461 @@ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Change the default font family in all browsers (opinionated). + * 2. Correct the line height in all browsers. + * 3. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +/* Document + ========================================================================== */ + +html { + font-family: sans-serif; /* 1 */ + line-height: 1.15; /* 2 */ + -ms-text-size-adjust: 100%; /* 3 */ + -webkit-text-size-adjust: 100%; /* 3 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * Remove the outline on focused links when they are also active or hovered + * in all browsers (opinionated). + */ + +a:active, +a:hover { + outline-width: 0; +} + +/** + * 1. Remove the bottom border in Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Change the border, margin, and padding in all browsers (opinionated). + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/web-ui/react/css/style.css b/web-ui/react/css/style.css new file mode 100644 index 00000000..61ac8587 --- /dev/null +++ b/web-ui/react/css/style.css @@ -0,0 +1,174 @@ +body { + font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; +} + +.field-group { + position:relative; + margin-bottom: 35px; +} + +label { + font-size: 0.9em; + margin-bottom: 10px; + display: inline-block; +} + +input { + display: block; + border: solid 1px #4da3b6; + width: 100%; + height: auto; + padding: 10px 5px; + margin-bottom: 20px; +} + +.animated-label { + color:#999; + position:absolute; + pointer-events:none; + left: 6px; + top:10px; + transition:0.2s ease all; + -moz-transition:0.2s ease all; + -webkit-transition:0.2s ease all; +} + +input:focus { + outline:none; +} + +input:focus ~ .animated-label, input:valid ~ .animated-label{ + top:-20px; + left: 0; + font-size:0.8em; + color:#4da3b6; +} + +.blue-button { + background: #178ca6; + color: white; + display: block; + text-decoration: none; + text-align: center; + padding: 10px 0 10px 0; + width: 104%; + margin: 0 auto; +} + +.blue-button:hover { + background: #4da3b6; +} + +a { + text-decoration: none; + color: #4da3b6; +} + +h1 { + font-size: 1.5em; + text-align: center; +} + +header { + width: 18%; + margin: 0 auto; +} + +.link-message { + text-align: center; + font-size: 0.8em; +} + +.logo { + width: 100%; + height: auto; + padding-top: 20%; + margin-bottom: 30px; +} + +.message h1 { + margin-bottom: 35px; +} + +.message p { + padding-left: 5%; + padding-right: 5%; + width: 40%; + margin: 0 auto; + text-align: center; + line-height: 1.8em; + font-size: 0.9em; +} + +.form-container { + width: 20%; + margin: 0 auto; + padding-top: 40px; +} + +.domain-label { + position: relative; + top: 26px; + padding-left: 20px; + left: 100%; +} + +.sent-email-icon { + width: 60px; +} + +.disabled { + pointer-events: none; + background: #d4d4d4; +} + +.link-message .disabled { + pointer-events: none; + color: #d4d4d4; + background: none; +} + +/* Medium Devices, Desktops */ +@media only screen and (max-width : 992px) { + header { + width: 20%; + } + + .form-container { + width: 30%; + } + + .message p { + width: 70% + } +} + +/* Small Devices, Tablets */ +@media only screen and (max-width : 768px) { + header { + width: 30%; + } + + .form-container { + width: 50%; + } + + .message p { + width: 80% + } +} + +/* Extra Small Devices, Phones */ +@media only screen and (max-width : 480px) { + header { + width: 60%; + } + + .form-container { + width: 80%; + } + + .message p { + width: 85% + } +} diff --git a/web-ui/react/images/pixelated-logo-orange.svg b/web-ui/react/images/pixelated-logo-orange.svg new file mode 100644 index 00000000..7e0ef43d --- /dev/null +++ b/web-ui/react/images/pixelated-logo-orange.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/web-ui/react/images/sent_email.png b/web-ui/react/images/sent_email.png new file mode 100644 index 00000000..ddaa11d0 Binary files /dev/null and b/web-ui/react/images/sent_email.png differ diff --git a/web-ui/react/index.html b/web-ui/react/index.html new file mode 100644 index 00000000..a1836721 --- /dev/null +++ b/web-ui/react/index.html @@ -0,0 +1,18 @@ + + + + + + Pixelated Mail + + + + + +
+
+
+ +
+ + diff --git a/web-ui/react/src/index.js b/web-ui/react/src/index.js new file mode 100644 index 00000000..ae52867f --- /dev/null +++ b/web-ui/react/src/index.js @@ -0,0 +1,163 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {createStore} from 'redux'; +import {Map} from 'immutable'; + + +class PixelatedComponent extends React.Component { + _updateStateFromStore() { + this.setState(this.props.store.getState().toJS()); + } + + componentWillMount() { + console.debug('mounting', this); + this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); + this._updateStateFromStore(); + } + + componentWillUnmount() { + console.debug('unmounting', this); + this.unsubscribe() + } +} + + +class InviteCodeForm extends PixelatedComponent { + render() { + return ( +
+
+ + +
+ +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE'}); + } +} + + +class CreateAccountForm extends PixelatedComponent { + render() { + return ( +
+ @domain.com +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT'}); + } +} + + +class BackupEmailForm extends PixelatedComponent { + render() { + return ( +
+ @domain.com +
+ + +
+ +
+ + +
+ + +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT'}); + } +} + + +class SignUp extends PixelatedComponent { + render() { + return ( +
+
+

{this.state.header}

+

{this.state.summary}

+
+
+ {this._form()} +
+
+ ); + } + + _form() { + switch(this.state.form) { + case 'invite_code': return ; + case 'create_account': return ; + case 'backup_email': return ; + default: throw Exception('TODO'); + } + } +} + + +const initialState = new Map({ + form: 'invite_code', + header: 'Welcome', + summary: ['Do you have an invite code?',
, 'Type it below'], +}); + + +const store = createStore((state=initialState, action) => { + switch (action.type) { + case 'SUBMIT_INVITE_CODE': + return state.merge({ + form: 'create_account', + header: 'Create your account', + summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', + }); + case 'SUBMIT_CREATE_ACCOUNT': + return state.merge({ + form: 'backup_email', + header: 'In case you lose your password...', + summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', + }); + default: + return state; + } +}); + + +ReactDOM.render( + , + document.getElementById('app') +); -- cgit v1.2.3 From d6a32b73c752dfe43517812d1300e18aab03e9e0 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 16 Nov 2016 17:16:23 +0100 Subject: add a form and use fetch --- .gitignore | 3 ++ web-ui/package.json | 3 +- web-ui/react/dummy.json | 1 + web-ui/react/src/index.js | 106 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 web-ui/react/dummy.json diff --git a/.gitignore b/.gitignore index 0c347bf8..d60eade0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ web-ui/node_modules web-ui/app/bower_components web-ui/target +web-ui/react/bundle.css +web-ui/react/bundle.js +web-ui/react/lib/ .tmp .sass-cache/ dist/ diff --git a/web-ui/package.json b/web-ui/package.json index c85889e9..7b454d8c 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -52,7 +52,8 @@ "immutable": "^3.8.1", "react": "^15.3.2", "react-dom": "^15.3.2", - "redux": "^3.6.0" + "redux": "^3.6.0", + "whatwg-fetch": "^2.0.0" }, "babel": { "presets": [ diff --git a/web-ui/react/dummy.json b/web-ui/react/dummy.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web-ui/react/dummy.json @@ -0,0 +1 @@ +{} diff --git a/web-ui/react/src/index.js b/web-ui/react/src/index.js index ae52867f..35de6407 100644 --- a/web-ui/react/src/index.js +++ b/web-ui/react/src/index.js @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {createStore} from 'redux'; import {Map} from 'immutable'; +import 'whatwg-fetch'; class PixelatedComponent extends React.Component { @@ -10,19 +11,37 @@ class PixelatedComponent extends React.Component { } componentWillMount() { - console.debug('mounting', this); this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); this._updateStateFromStore(); } componentWillUnmount() { - console.debug('unmounting', this); this.unsubscribe() } } -class InviteCodeForm extends PixelatedComponent { +class PixelatedForm extends PixelatedComponent { + _fetchAndDispatch(url, actionProperties) { + const immutableActionProperties = new Map(actionProperties); + this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); + fetch(url).then((response) => { + console.debug('got a reply', response); + return response.json() + }).then((json) => { + console.debug('got json', json); + setTimeout(() => { + this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); + }, 3000); + }).catch((error) => { + console.error('something went wrong', error); + this.props.store.dispatch(immutableActionProperties.merge({status: 'ERROR', error: error}).toJS()); + }); + } +} + + +class InviteCodeForm extends PixelatedForm { render() { return (
@@ -38,12 +57,12 @@ class InviteCodeForm extends PixelatedComponent { _handleClick(event) { event.stopPropagation(); event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE'}); + this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); } } -class CreateAccountForm extends PixelatedComponent { +class CreateAccountForm extends PixelatedForm { render() { return ( @@ -58,11 +77,6 @@ class CreateAccountForm extends PixelatedComponent { -
- - -
-
); @@ -71,27 +85,44 @@ class CreateAccountForm extends PixelatedComponent { _handleClick(event) { event.stopPropagation(); event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT'}); + this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT', username: event.target['username'].value, password: event.target['password'].value}); } } -class BackupEmailForm extends PixelatedComponent { +class BackupEmailForm extends PixelatedForm { render() { return (
- @domain.com
- - + +
-
- - -
+ +

+ I didn't receive anything. Send the email again +

+
+ ); + } - + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this._fetchAndDispatch('dummy.json', {type: 'SUBMIT_BACKUP_EMAIL', backupEmail: event.target['backup-email'].value}); + } +} + + +class BackupEmailSentForm extends PixelatedForm { + render() { + return ( +
+ {this.state.isFetching || I received the codes.
Go to my inbox
} +

+ I didn't receive anything. Send the email again +

); } @@ -99,7 +130,6 @@ class BackupEmailForm extends PixelatedComponent { _handleClick(event) { event.stopPropagation(); event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT'}); } } @@ -110,6 +140,7 @@ class SignUp extends PixelatedComponent {

{this.state.header}

+ {this.state.icon}

{this.state.summary}

@@ -124,16 +155,19 @@ class SignUp extends PixelatedComponent { case 'invite_code': return ; case 'create_account': return ; case 'backup_email': return ; - default: throw Exception('TODO'); + case 'backup_email_sent': return ; + default: throw Error('TODO'); } } } const initialState = new Map({ + isFetching: false, form: 'invite_code', header: 'Welcome', - summary: ['Do you have an invite code?',
, 'Type it below'], + icon: null, + summary: ['Do you have an invite code?',
, 'Type it below'], }); @@ -141,16 +175,42 @@ const store = createStore((state=initialState, action) => { switch (action.type) { case 'SUBMIT_INVITE_CODE': return state.merge({ + inviteCode: action.inviteCode, form: 'create_account', header: 'Create your account', summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', }); case 'SUBMIT_CREATE_ACCOUNT': return state.merge({ + username: action.username, + password: action.password, form: 'backup_email', header: 'In case you lose your password...', summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', }); + case 'SUBMIT_BACKUP_EMAIL': + switch (action.status) { + case 'STARTED': + return state.merge({ + isFetching: true, + backupEmail: action.backupEmail, + form: 'backup_email_sent', + icon:

, + summary: 'An email was sent to the email you provided. Check your spam folder, just in case.', + }); + case 'SUCCESS': + return state.merge({ + isFetching: false, + }); + case 'ERROR': + return state.merge({ + isFetching: false, + }); + default: + return state; + } + case 'SUBMIT_BACKUP_EMAIL_SENT': + return state.merge({}); default: return state; } -- cgit v1.2.3 From 4737d89b1eb4b518792314ceed7a2fef4f1eb13b Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 16 Nov 2016 17:48:00 +0100 Subject: rename react subdir to signup --- web-ui/package.json | 4 +- web-ui/react/css/normalize.css | 461 ------------------------- web-ui/react/css/style.css | 174 ---------- web-ui/react/dummy.json | 1 - web-ui/react/images/pixelated-logo-orange.svg | 29 -- web-ui/react/images/sent_email.png | Bin 9160 -> 0 bytes web-ui/react/index.html | 18 - web-ui/react/src/index.js | 223 ------------ web-ui/signup/css/normalize.css | 461 +++++++++++++++++++++++++ web-ui/signup/css/style.css | 174 ++++++++++ web-ui/signup/dummy.json | 1 + web-ui/signup/images/pixelated-logo-orange.svg | 29 ++ web-ui/signup/images/sent_email.png | Bin 0 -> 9160 bytes web-ui/signup/index.html | 18 + web-ui/signup/src/index.js | 223 ++++++++++++ 15 files changed, 908 insertions(+), 908 deletions(-) delete mode 100644 web-ui/react/css/normalize.css delete mode 100644 web-ui/react/css/style.css delete mode 100644 web-ui/react/dummy.json delete mode 100644 web-ui/react/images/pixelated-logo-orange.svg delete mode 100644 web-ui/react/images/sent_email.png delete mode 100644 web-ui/react/index.html delete mode 100644 web-ui/react/src/index.js create mode 100644 web-ui/signup/css/normalize.css create mode 100644 web-ui/signup/css/style.css create mode 100644 web-ui/signup/dummy.json create mode 100644 web-ui/signup/images/pixelated-logo-orange.svg create mode 100644 web-ui/signup/images/sent_email.png create mode 100644 web-ui/signup/index.html create mode 100644 web-ui/signup/src/index.js diff --git a/web-ui/package.json b/web-ui/package.json index 7b454d8c..c655c253 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -35,8 +35,8 @@ "handlebars-watch": "node_modules/.bin/watch 'npm run handlebars' app/templates", "compass": "compass compile", "compass-watch": "compass watch", - "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass && npm run build-react", - "build-react": "babel react/src -d react/lib && browserify react/lib/index.js >react/bundle.js", + "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass && npm run build-signup", + "build-signup": "babel signup/src -d signup/lib && browserify signup/lib/index.js >signup/bundle.js", "jshint": "node_modules/jshint/bin/jshint --config=.jshintrc app test", "clean": "rm -rf .tmp/ 'dist/*' app/js/generated/hbs/* app/css/*", "buildmain": "node_modules/requirejs/bin/r.js -o config/buildoptions.js", diff --git a/web-ui/react/css/normalize.css b/web-ui/react/css/normalize.css deleted file mode 100644 index 9b77e0eb..00000000 --- a/web-ui/react/css/normalize.css +++ /dev/null @@ -1,461 +0,0 @@ -/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Change the default font family in all browsers (opinionated). - * 2. Correct the line height in all browsers. - * 3. Prevent adjustments of font size after orientation changes in - * IE on Windows Phone and in iOS. - */ - -/* Document - ========================================================================== */ - -html { - font-family: sans-serif; /* 1 */ - line-height: 1.15; /* 2 */ - -ms-text-size-adjust: 100%; /* 3 */ - -webkit-text-size-adjust: 100%; /* 3 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers (opinionated). - */ - -body { - margin: 0; -} - -/** - * Add the correct display in IE 9-. - */ - -article, -aside, -footer, -header, -nav, -section { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - * 1. Add the correct display in IE. - */ - -figcaption, -figure, -main { /* 1 */ - display: block; -} - -/** - * Add the correct margin in IE 8. - */ - -figure { - margin: 1em 40px; -} - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * 1. Remove the gray background on active links in IE 10. - * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. - */ - -a { - background-color: transparent; /* 1 */ - -webkit-text-decoration-skip: objects; /* 2 */ -} - -/** - * Remove the outline on focused links when they are also active or hovered - * in all browsers (opinionated). - */ - -a:active, -a:hover { - outline-width: 0; -} - -/** - * 1. Remove the bottom border in Firefox 39-. - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Prevent the duplicate application of `bolder` by the next rule in Safari 6. - */ - -b, -strong { - font-weight: inherit; -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font style in Android 4.3-. - */ - -dfn { - font-style: italic; -} - -/** - * Add the correct background and color in IE 9-. - */ - -mark { - background-color: #ff0; - color: #000; -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - */ - -audio, -video { - display: inline-block; -} - -/** - * Add the correct display in iOS 4-7. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Remove the border on images inside links in IE 10-. - */ - -img { - border-style: none; -} - -/** - * Hide the overflow in IE. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers (opinionated). - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: sans-serif; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` - * controls in Android 4. - * 2. Correct the inability to style clickable types in iOS and Safari. - */ - -button, -html [type="button"], /* 1 */ -[type="reset"], -[type="submit"] { - -webkit-appearance: button; /* 2 */ -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Change the border, margin, and padding in all browsers (opinionated). - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * 1. Add the correct display in IE 9-. - * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Remove the default vertical scrollbar in IE. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10-. - * 2. Remove the padding in IE 10-. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in IE 9-. - * 1. Add the correct display in Edge, IE, and Firefox. - */ - -details, /* 1 */ -menu { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Scripting - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - */ - -canvas { - display: inline-block; -} - -/** - * Add the correct display in IE. - */ - -template { - display: none; -} - -/* Hidden - ========================================================================== */ - -/** - * Add the correct display in IE 10-. - */ - -[hidden] { - display: none; -} diff --git a/web-ui/react/css/style.css b/web-ui/react/css/style.css deleted file mode 100644 index 61ac8587..00000000 --- a/web-ui/react/css/style.css +++ /dev/null @@ -1,174 +0,0 @@ -body { - font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; -} - -.field-group { - position:relative; - margin-bottom: 35px; -} - -label { - font-size: 0.9em; - margin-bottom: 10px; - display: inline-block; -} - -input { - display: block; - border: solid 1px #4da3b6; - width: 100%; - height: auto; - padding: 10px 5px; - margin-bottom: 20px; -} - -.animated-label { - color:#999; - position:absolute; - pointer-events:none; - left: 6px; - top:10px; - transition:0.2s ease all; - -moz-transition:0.2s ease all; - -webkit-transition:0.2s ease all; -} - -input:focus { - outline:none; -} - -input:focus ~ .animated-label, input:valid ~ .animated-label{ - top:-20px; - left: 0; - font-size:0.8em; - color:#4da3b6; -} - -.blue-button { - background: #178ca6; - color: white; - display: block; - text-decoration: none; - text-align: center; - padding: 10px 0 10px 0; - width: 104%; - margin: 0 auto; -} - -.blue-button:hover { - background: #4da3b6; -} - -a { - text-decoration: none; - color: #4da3b6; -} - -h1 { - font-size: 1.5em; - text-align: center; -} - -header { - width: 18%; - margin: 0 auto; -} - -.link-message { - text-align: center; - font-size: 0.8em; -} - -.logo { - width: 100%; - height: auto; - padding-top: 20%; - margin-bottom: 30px; -} - -.message h1 { - margin-bottom: 35px; -} - -.message p { - padding-left: 5%; - padding-right: 5%; - width: 40%; - margin: 0 auto; - text-align: center; - line-height: 1.8em; - font-size: 0.9em; -} - -.form-container { - width: 20%; - margin: 0 auto; - padding-top: 40px; -} - -.domain-label { - position: relative; - top: 26px; - padding-left: 20px; - left: 100%; -} - -.sent-email-icon { - width: 60px; -} - -.disabled { - pointer-events: none; - background: #d4d4d4; -} - -.link-message .disabled { - pointer-events: none; - color: #d4d4d4; - background: none; -} - -/* Medium Devices, Desktops */ -@media only screen and (max-width : 992px) { - header { - width: 20%; - } - - .form-container { - width: 30%; - } - - .message p { - width: 70% - } -} - -/* Small Devices, Tablets */ -@media only screen and (max-width : 768px) { - header { - width: 30%; - } - - .form-container { - width: 50%; - } - - .message p { - width: 80% - } -} - -/* Extra Small Devices, Phones */ -@media only screen and (max-width : 480px) { - header { - width: 60%; - } - - .form-container { - width: 80%; - } - - .message p { - width: 85% - } -} diff --git a/web-ui/react/dummy.json b/web-ui/react/dummy.json deleted file mode 100644 index 0967ef42..00000000 --- a/web-ui/react/dummy.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/web-ui/react/images/pixelated-logo-orange.svg b/web-ui/react/images/pixelated-logo-orange.svg deleted file mode 100644 index 7e0ef43d..00000000 --- a/web-ui/react/images/pixelated-logo-orange.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/web-ui/react/images/sent_email.png b/web-ui/react/images/sent_email.png deleted file mode 100644 index ddaa11d0..00000000 Binary files a/web-ui/react/images/sent_email.png and /dev/null differ diff --git a/web-ui/react/index.html b/web-ui/react/index.html deleted file mode 100644 index a1836721..00000000 --- a/web-ui/react/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Pixelated Mail - - - - - -
-
-
- -
- - diff --git a/web-ui/react/src/index.js b/web-ui/react/src/index.js deleted file mode 100644 index 35de6407..00000000 --- a/web-ui/react/src/index.js +++ /dev/null @@ -1,223 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import {createStore} from 'redux'; -import {Map} from 'immutable'; -import 'whatwg-fetch'; - - -class PixelatedComponent extends React.Component { - _updateStateFromStore() { - this.setState(this.props.store.getState().toJS()); - } - - componentWillMount() { - this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); - this._updateStateFromStore(); - } - - componentWillUnmount() { - this.unsubscribe() - } -} - - -class PixelatedForm extends PixelatedComponent { - _fetchAndDispatch(url, actionProperties) { - const immutableActionProperties = new Map(actionProperties); - this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); - fetch(url).then((response) => { - console.debug('got a reply', response); - return response.json() - }).then((json) => { - console.debug('got json', json); - setTimeout(() => { - this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); - }, 3000); - }).catch((error) => { - console.error('something went wrong', error); - this.props.store.dispatch(immutableActionProperties.merge({status: 'ERROR', error: error}).toJS()); - }); - } -} - - -class InviteCodeForm extends PixelatedForm { - render() { - return ( -
-
- - -
- -
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); - } -} - - -class CreateAccountForm extends PixelatedForm { - render() { - return ( -
- @domain.com -
- - -
- -
- - -
- - -
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT', username: event.target['username'].value, password: event.target['password'].value}); - } -} - - -class BackupEmailForm extends PixelatedForm { - render() { - return ( -
-
- - -
- - -

- I didn't receive anything. Send the email again -

-
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - this._fetchAndDispatch('dummy.json', {type: 'SUBMIT_BACKUP_EMAIL', backupEmail: event.target['backup-email'].value}); - } -} - - -class BackupEmailSentForm extends PixelatedForm { - render() { - return ( -
- {this.state.isFetching || I received the codes.
Go to my inbox
} -

- I didn't receive anything. Send the email again -

-
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - } -} - - -class SignUp extends PixelatedComponent { - render() { - return ( -
-
-

{this.state.header}

- {this.state.icon} -

{this.state.summary}

-
-
- {this._form()} -
-
- ); - } - - _form() { - switch(this.state.form) { - case 'invite_code': return ; - case 'create_account': return ; - case 'backup_email': return ; - case 'backup_email_sent': return ; - default: throw Error('TODO'); - } - } -} - - -const initialState = new Map({ - isFetching: false, - form: 'invite_code', - header: 'Welcome', - icon: null, - summary: ['Do you have an invite code?',
, 'Type it below'], -}); - - -const store = createStore((state=initialState, action) => { - switch (action.type) { - case 'SUBMIT_INVITE_CODE': - return state.merge({ - inviteCode: action.inviteCode, - form: 'create_account', - header: 'Create your account', - summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', - }); - case 'SUBMIT_CREATE_ACCOUNT': - return state.merge({ - username: action.username, - password: action.password, - form: 'backup_email', - header: 'In case you lose your password...', - summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', - }); - case 'SUBMIT_BACKUP_EMAIL': - switch (action.status) { - case 'STARTED': - return state.merge({ - isFetching: true, - backupEmail: action.backupEmail, - form: 'backup_email_sent', - icon:

, - summary: 'An email was sent to the email you provided. Check your spam folder, just in case.', - }); - case 'SUCCESS': - return state.merge({ - isFetching: false, - }); - case 'ERROR': - return state.merge({ - isFetching: false, - }); - default: - return state; - } - case 'SUBMIT_BACKUP_EMAIL_SENT': - return state.merge({}); - default: - return state; - } -}); - - -ReactDOM.render( - , - document.getElementById('app') -); diff --git a/web-ui/signup/css/normalize.css b/web-ui/signup/css/normalize.css new file mode 100644 index 00000000..9b77e0eb --- /dev/null +++ b/web-ui/signup/css/normalize.css @@ -0,0 +1,461 @@ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Change the default font family in all browsers (opinionated). + * 2. Correct the line height in all browsers. + * 3. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +/* Document + ========================================================================== */ + +html { + font-family: sans-serif; /* 1 */ + line-height: 1.15; /* 2 */ + -ms-text-size-adjust: 100%; /* 3 */ + -webkit-text-size-adjust: 100%; /* 3 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * Remove the outline on focused links when they are also active or hovered + * in all browsers (opinionated). + */ + +a:active, +a:hover { + outline-width: 0; +} + +/** + * 1. Remove the bottom border in Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Change the border, margin, and padding in all browsers (opinionated). + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/web-ui/signup/css/style.css b/web-ui/signup/css/style.css new file mode 100644 index 00000000..61ac8587 --- /dev/null +++ b/web-ui/signup/css/style.css @@ -0,0 +1,174 @@ +body { + font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; +} + +.field-group { + position:relative; + margin-bottom: 35px; +} + +label { + font-size: 0.9em; + margin-bottom: 10px; + display: inline-block; +} + +input { + display: block; + border: solid 1px #4da3b6; + width: 100%; + height: auto; + padding: 10px 5px; + margin-bottom: 20px; +} + +.animated-label { + color:#999; + position:absolute; + pointer-events:none; + left: 6px; + top:10px; + transition:0.2s ease all; + -moz-transition:0.2s ease all; + -webkit-transition:0.2s ease all; +} + +input:focus { + outline:none; +} + +input:focus ~ .animated-label, input:valid ~ .animated-label{ + top:-20px; + left: 0; + font-size:0.8em; + color:#4da3b6; +} + +.blue-button { + background: #178ca6; + color: white; + display: block; + text-decoration: none; + text-align: center; + padding: 10px 0 10px 0; + width: 104%; + margin: 0 auto; +} + +.blue-button:hover { + background: #4da3b6; +} + +a { + text-decoration: none; + color: #4da3b6; +} + +h1 { + font-size: 1.5em; + text-align: center; +} + +header { + width: 18%; + margin: 0 auto; +} + +.link-message { + text-align: center; + font-size: 0.8em; +} + +.logo { + width: 100%; + height: auto; + padding-top: 20%; + margin-bottom: 30px; +} + +.message h1 { + margin-bottom: 35px; +} + +.message p { + padding-left: 5%; + padding-right: 5%; + width: 40%; + margin: 0 auto; + text-align: center; + line-height: 1.8em; + font-size: 0.9em; +} + +.form-container { + width: 20%; + margin: 0 auto; + padding-top: 40px; +} + +.domain-label { + position: relative; + top: 26px; + padding-left: 20px; + left: 100%; +} + +.sent-email-icon { + width: 60px; +} + +.disabled { + pointer-events: none; + background: #d4d4d4; +} + +.link-message .disabled { + pointer-events: none; + color: #d4d4d4; + background: none; +} + +/* Medium Devices, Desktops */ +@media only screen and (max-width : 992px) { + header { + width: 20%; + } + + .form-container { + width: 30%; + } + + .message p { + width: 70% + } +} + +/* Small Devices, Tablets */ +@media only screen and (max-width : 768px) { + header { + width: 30%; + } + + .form-container { + width: 50%; + } + + .message p { + width: 80% + } +} + +/* Extra Small Devices, Phones */ +@media only screen and (max-width : 480px) { + header { + width: 60%; + } + + .form-container { + width: 80%; + } + + .message p { + width: 85% + } +} diff --git a/web-ui/signup/dummy.json b/web-ui/signup/dummy.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web-ui/signup/dummy.json @@ -0,0 +1 @@ +{} diff --git a/web-ui/signup/images/pixelated-logo-orange.svg b/web-ui/signup/images/pixelated-logo-orange.svg new file mode 100644 index 00000000..7e0ef43d --- /dev/null +++ b/web-ui/signup/images/pixelated-logo-orange.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/web-ui/signup/images/sent_email.png b/web-ui/signup/images/sent_email.png new file mode 100644 index 00000000..ddaa11d0 Binary files /dev/null and b/web-ui/signup/images/sent_email.png differ diff --git a/web-ui/signup/index.html b/web-ui/signup/index.html new file mode 100644 index 00000000..a1836721 --- /dev/null +++ b/web-ui/signup/index.html @@ -0,0 +1,18 @@ + + + + + + Pixelated Mail + + + + + +
+
+
+ +
+ + diff --git a/web-ui/signup/src/index.js b/web-ui/signup/src/index.js new file mode 100644 index 00000000..35de6407 --- /dev/null +++ b/web-ui/signup/src/index.js @@ -0,0 +1,223 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {createStore} from 'redux'; +import {Map} from 'immutable'; +import 'whatwg-fetch'; + + +class PixelatedComponent extends React.Component { + _updateStateFromStore() { + this.setState(this.props.store.getState().toJS()); + } + + componentWillMount() { + this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); + this._updateStateFromStore(); + } + + componentWillUnmount() { + this.unsubscribe() + } +} + + +class PixelatedForm extends PixelatedComponent { + _fetchAndDispatch(url, actionProperties) { + const immutableActionProperties = new Map(actionProperties); + this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); + fetch(url).then((response) => { + console.debug('got a reply', response); + return response.json() + }).then((json) => { + console.debug('got json', json); + setTimeout(() => { + this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); + }, 3000); + }).catch((error) => { + console.error('something went wrong', error); + this.props.store.dispatch(immutableActionProperties.merge({status: 'ERROR', error: error}).toJS()); + }); + } +} + + +class InviteCodeForm extends PixelatedForm { + render() { + return ( +
+
+ + +
+ +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); + } +} + + +class CreateAccountForm extends PixelatedForm { + render() { + return ( +
+ @domain.com +
+ + +
+ +
+ + +
+ + +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT', username: event.target['username'].value, password: event.target['password'].value}); + } +} + + +class BackupEmailForm extends PixelatedForm { + render() { + return ( +
+
+ + +
+ + +

+ I didn't receive anything. Send the email again +

+
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this._fetchAndDispatch('dummy.json', {type: 'SUBMIT_BACKUP_EMAIL', backupEmail: event.target['backup-email'].value}); + } +} + + +class BackupEmailSentForm extends PixelatedForm { + render() { + return ( +
+ {this.state.isFetching || I received the codes.
Go to my inbox
} +

+ I didn't receive anything. Send the email again +

+
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + } +} + + +class SignUp extends PixelatedComponent { + render() { + return ( +
+
+

{this.state.header}

+ {this.state.icon} +

{this.state.summary}

+
+
+ {this._form()} +
+
+ ); + } + + _form() { + switch(this.state.form) { + case 'invite_code': return ; + case 'create_account': return ; + case 'backup_email': return ; + case 'backup_email_sent': return ; + default: throw Error('TODO'); + } + } +} + + +const initialState = new Map({ + isFetching: false, + form: 'invite_code', + header: 'Welcome', + icon: null, + summary: ['Do you have an invite code?',
, 'Type it below'], +}); + + +const store = createStore((state=initialState, action) => { + switch (action.type) { + case 'SUBMIT_INVITE_CODE': + return state.merge({ + inviteCode: action.inviteCode, + form: 'create_account', + header: 'Create your account', + summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', + }); + case 'SUBMIT_CREATE_ACCOUNT': + return state.merge({ + username: action.username, + password: action.password, + form: 'backup_email', + header: 'In case you lose your password...', + summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', + }); + case 'SUBMIT_BACKUP_EMAIL': + switch (action.status) { + case 'STARTED': + return state.merge({ + isFetching: true, + backupEmail: action.backupEmail, + form: 'backup_email_sent', + icon:

, + summary: 'An email was sent to the email you provided. Check your spam folder, just in case.', + }); + case 'SUCCESS': + return state.merge({ + isFetching: false, + }); + case 'ERROR': + return state.merge({ + isFetching: false, + }); + default: + return state; + } + case 'SUBMIT_BACKUP_EMAIL_SENT': + return state.merge({}); + default: + return state; + } +}); + + +ReactDOM.render( + , + document.getElementById('app') +); -- cgit v1.2.3 From 44112ced551f36972770704b2ee7ef61a455df83 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 16 Nov 2016 17:48:00 +0100 Subject: rename react subdir to signup --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index d60eade0..b53ecd20 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,9 @@ web-ui/node_modules web-ui/app/bower_components web-ui/target -web-ui/react/bundle.css -web-ui/react/bundle.js -web-ui/react/lib/ +web-ui/signup/bundle.css +web-ui/signup/bundle.js +web-ui/signup/lib/ .tmp .sass-cache/ dist/ -- cgit v1.2.3 From 6cdfb2110b0f96502eeaaf98f59a05704534cdff Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Fri, 18 Nov 2016 13:47:32 +0100 Subject: serve signup page through twisted --- .gitignore | 6 +- service/pixelated/resources/login_resource.py | 13 + service/pixelated/resources/root_resource.py | 14 + web-ui/package.json | 2 +- web-ui/public/signup.css | 174 ++++++++++ web-ui/public/signup.html | 19 + web-ui/signup/css/normalize.css | 461 ------------------------- web-ui/signup/css/style.css | 174 ---------- web-ui/signup/images/pixelated-logo-orange.svg | 29 -- web-ui/signup/images/sent_email.png | Bin 9160 -> 0 bytes web-ui/signup/index.html | 18 - web-ui/signup/src/index.js | 223 ------------ web-ui/src/js/index.js | 223 ++++++++++++ 13 files changed, 446 insertions(+), 910 deletions(-) create mode 100644 web-ui/public/signup.css create mode 100644 web-ui/public/signup.html delete mode 100644 web-ui/signup/css/normalize.css delete mode 100644 web-ui/signup/css/style.css delete mode 100644 web-ui/signup/images/pixelated-logo-orange.svg delete mode 100644 web-ui/signup/images/sent_email.png delete mode 100644 web-ui/signup/index.html delete mode 100644 web-ui/signup/src/index.js create mode 100644 web-ui/src/js/index.js diff --git a/.gitignore b/.gitignore index b53ecd20..155686f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,8 @@ *.egg-info web-ui/node_modules web-ui/app/bower_components -web-ui/target -web-ui/signup/bundle.css -web-ui/signup/bundle.js -web-ui/signup/lib/ +web-ui/lib/ +web-ui/public/signup.js .tmp .sass-cache/ dist/ diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index d5555b90..905b872c 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -39,6 +39,17 @@ def _get_startup_folder(): return os.path.join(path, '..', 'assets') +def _get_public_folder(): + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "public")) + # this is a workaround for packaging + if not os.path.exists(static_folder): + static_folder = os.path.abspath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "public")) + if not os.path.exists(static_folder): + static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + return static_folder + + def _get_static_folder(): static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) # this is a workaround for packaging @@ -107,6 +118,7 @@ class LoginResource(BaseResource): def __init__(self, services_factory, provider=None, disclaimer_banner=None, authenticator=None): BaseResource.__init__(self, services_factory) self._static_folder = _get_static_folder() + self._public_folder = _get_public_folder() self._startup_folder = _get_startup_folder() self._disclaimer_banner = disclaimer_banner self._provider = provider @@ -114,6 +126,7 @@ class LoginResource(BaseResource): self._bootstrap_user_services = BootstrapUserServices(services_factory, provider) self.putChild('startup-assets', File(self._startup_folder)) + self.putChild('public-assets', File(self._public_folder)) with open(os.path.join(self._startup_folder, 'Interstitial.html')) as f: self.interstitial = f.read() diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 8df76c70..8fa80bb2 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -51,6 +51,7 @@ class RootResource(BaseResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) self._startup_assets_folder = self._get_startup_folder() + self._public_assets_folder = self._get_public_folder() self._static_folder = self._get_static_folder() self._html_template = open(os.path.join(self._static_folder, 'index.html')).read() self._services_factory = services_factory @@ -61,6 +62,7 @@ class RootResource(BaseResource): def _startup_mode(self): self.putChild('startup-assets', File(self._startup_assets_folder)) + self.putChild('public-assets', File(self._public_assets_folder)) self._mode = MODE_STARTUP def getChild(self, path, request): @@ -106,10 +108,22 @@ class RootResource(BaseResource): self._mode = MODE_RUNNING + # TODO: use the public folder for this def _get_startup_folder(self): path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(path, '..', 'assets') + def _get_public_folder(self): + public_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "public")) + # this is a workaround for packaging + if not os.path.exists(public_folder): + public_folder = os.path.abspath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "public")) + if not os.path.exists(public_folder): + # TODO: how is this packaged? + public_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + return public_folder + def _get_static_folder(self): static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) # this is a workaround for packaging diff --git a/web-ui/package.json b/web-ui/package.json index c655c253..b937502f 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -36,7 +36,7 @@ "compass": "compass compile", "compass-watch": "compass watch", "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass && npm run build-signup", - "build-signup": "babel signup/src -d signup/lib && browserify signup/lib/index.js >signup/bundle.js", + "build-signup": "babel src/js -d lib/js && browserify lib/js/index.js >public/signup.js", "jshint": "node_modules/jshint/bin/jshint --config=.jshintrc app test", "clean": "rm -rf .tmp/ 'dist/*' app/js/generated/hbs/* app/css/*", "buildmain": "node_modules/requirejs/bin/r.js -o config/buildoptions.js", diff --git a/web-ui/public/signup.css b/web-ui/public/signup.css new file mode 100644 index 00000000..61ac8587 --- /dev/null +++ b/web-ui/public/signup.css @@ -0,0 +1,174 @@ +body { + font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; +} + +.field-group { + position:relative; + margin-bottom: 35px; +} + +label { + font-size: 0.9em; + margin-bottom: 10px; + display: inline-block; +} + +input { + display: block; + border: solid 1px #4da3b6; + width: 100%; + height: auto; + padding: 10px 5px; + margin-bottom: 20px; +} + +.animated-label { + color:#999; + position:absolute; + pointer-events:none; + left: 6px; + top:10px; + transition:0.2s ease all; + -moz-transition:0.2s ease all; + -webkit-transition:0.2s ease all; +} + +input:focus { + outline:none; +} + +input:focus ~ .animated-label, input:valid ~ .animated-label{ + top:-20px; + left: 0; + font-size:0.8em; + color:#4da3b6; +} + +.blue-button { + background: #178ca6; + color: white; + display: block; + text-decoration: none; + text-align: center; + padding: 10px 0 10px 0; + width: 104%; + margin: 0 auto; +} + +.blue-button:hover { + background: #4da3b6; +} + +a { + text-decoration: none; + color: #4da3b6; +} + +h1 { + font-size: 1.5em; + text-align: center; +} + +header { + width: 18%; + margin: 0 auto; +} + +.link-message { + text-align: center; + font-size: 0.8em; +} + +.logo { + width: 100%; + height: auto; + padding-top: 20%; + margin-bottom: 30px; +} + +.message h1 { + margin-bottom: 35px; +} + +.message p { + padding-left: 5%; + padding-right: 5%; + width: 40%; + margin: 0 auto; + text-align: center; + line-height: 1.8em; + font-size: 0.9em; +} + +.form-container { + width: 20%; + margin: 0 auto; + padding-top: 40px; +} + +.domain-label { + position: relative; + top: 26px; + padding-left: 20px; + left: 100%; +} + +.sent-email-icon { + width: 60px; +} + +.disabled { + pointer-events: none; + background: #d4d4d4; +} + +.link-message .disabled { + pointer-events: none; + color: #d4d4d4; + background: none; +} + +/* Medium Devices, Desktops */ +@media only screen and (max-width : 992px) { + header { + width: 20%; + } + + .form-container { + width: 30%; + } + + .message p { + width: 70% + } +} + +/* Small Devices, Tablets */ +@media only screen and (max-width : 768px) { + header { + width: 30%; + } + + .form-container { + width: 50%; + } + + .message p { + width: 80% + } +} + +/* Extra Small Devices, Phones */ +@media only screen and (max-width : 480px) { + header { + width: 60%; + } + + .form-container { + width: 80%; + } + + .message p { + width: 85% + } +} diff --git a/web-ui/public/signup.html b/web-ui/public/signup.html new file mode 100644 index 00000000..9bc6cdad --- /dev/null +++ b/web-ui/public/signup.html @@ -0,0 +1,19 @@ + + + + + + Pixelated Mail + + + + + + +
+
+
+ +
+ + diff --git a/web-ui/signup/css/normalize.css b/web-ui/signup/css/normalize.css deleted file mode 100644 index 9b77e0eb..00000000 --- a/web-ui/signup/css/normalize.css +++ /dev/null @@ -1,461 +0,0 @@ -/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Change the default font family in all browsers (opinionated). - * 2. Correct the line height in all browsers. - * 3. Prevent adjustments of font size after orientation changes in - * IE on Windows Phone and in iOS. - */ - -/* Document - ========================================================================== */ - -html { - font-family: sans-serif; /* 1 */ - line-height: 1.15; /* 2 */ - -ms-text-size-adjust: 100%; /* 3 */ - -webkit-text-size-adjust: 100%; /* 3 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers (opinionated). - */ - -body { - margin: 0; -} - -/** - * Add the correct display in IE 9-. - */ - -article, -aside, -footer, -header, -nav, -section { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - * 1. Add the correct display in IE. - */ - -figcaption, -figure, -main { /* 1 */ - display: block; -} - -/** - * Add the correct margin in IE 8. - */ - -figure { - margin: 1em 40px; -} - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * 1. Remove the gray background on active links in IE 10. - * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. - */ - -a { - background-color: transparent; /* 1 */ - -webkit-text-decoration-skip: objects; /* 2 */ -} - -/** - * Remove the outline on focused links when they are also active or hovered - * in all browsers (opinionated). - */ - -a:active, -a:hover { - outline-width: 0; -} - -/** - * 1. Remove the bottom border in Firefox 39-. - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Prevent the duplicate application of `bolder` by the next rule in Safari 6. - */ - -b, -strong { - font-weight: inherit; -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font style in Android 4.3-. - */ - -dfn { - font-style: italic; -} - -/** - * Add the correct background and color in IE 9-. - */ - -mark { - background-color: #ff0; - color: #000; -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - */ - -audio, -video { - display: inline-block; -} - -/** - * Add the correct display in iOS 4-7. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Remove the border on images inside links in IE 10-. - */ - -img { - border-style: none; -} - -/** - * Hide the overflow in IE. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers (opinionated). - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: sans-serif; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` - * controls in Android 4. - * 2. Correct the inability to style clickable types in iOS and Safari. - */ - -button, -html [type="button"], /* 1 */ -[type="reset"], -[type="submit"] { - -webkit-appearance: button; /* 2 */ -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Change the border, margin, and padding in all browsers (opinionated). - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * 1. Add the correct display in IE 9-. - * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Remove the default vertical scrollbar in IE. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10-. - * 2. Remove the padding in IE 10-. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in IE 9-. - * 1. Add the correct display in Edge, IE, and Firefox. - */ - -details, /* 1 */ -menu { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Scripting - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - */ - -canvas { - display: inline-block; -} - -/** - * Add the correct display in IE. - */ - -template { - display: none; -} - -/* Hidden - ========================================================================== */ - -/** - * Add the correct display in IE 10-. - */ - -[hidden] { - display: none; -} diff --git a/web-ui/signup/css/style.css b/web-ui/signup/css/style.css deleted file mode 100644 index 61ac8587..00000000 --- a/web-ui/signup/css/style.css +++ /dev/null @@ -1,174 +0,0 @@ -body { - font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; -} - -.field-group { - position:relative; - margin-bottom: 35px; -} - -label { - font-size: 0.9em; - margin-bottom: 10px; - display: inline-block; -} - -input { - display: block; - border: solid 1px #4da3b6; - width: 100%; - height: auto; - padding: 10px 5px; - margin-bottom: 20px; -} - -.animated-label { - color:#999; - position:absolute; - pointer-events:none; - left: 6px; - top:10px; - transition:0.2s ease all; - -moz-transition:0.2s ease all; - -webkit-transition:0.2s ease all; -} - -input:focus { - outline:none; -} - -input:focus ~ .animated-label, input:valid ~ .animated-label{ - top:-20px; - left: 0; - font-size:0.8em; - color:#4da3b6; -} - -.blue-button { - background: #178ca6; - color: white; - display: block; - text-decoration: none; - text-align: center; - padding: 10px 0 10px 0; - width: 104%; - margin: 0 auto; -} - -.blue-button:hover { - background: #4da3b6; -} - -a { - text-decoration: none; - color: #4da3b6; -} - -h1 { - font-size: 1.5em; - text-align: center; -} - -header { - width: 18%; - margin: 0 auto; -} - -.link-message { - text-align: center; - font-size: 0.8em; -} - -.logo { - width: 100%; - height: auto; - padding-top: 20%; - margin-bottom: 30px; -} - -.message h1 { - margin-bottom: 35px; -} - -.message p { - padding-left: 5%; - padding-right: 5%; - width: 40%; - margin: 0 auto; - text-align: center; - line-height: 1.8em; - font-size: 0.9em; -} - -.form-container { - width: 20%; - margin: 0 auto; - padding-top: 40px; -} - -.domain-label { - position: relative; - top: 26px; - padding-left: 20px; - left: 100%; -} - -.sent-email-icon { - width: 60px; -} - -.disabled { - pointer-events: none; - background: #d4d4d4; -} - -.link-message .disabled { - pointer-events: none; - color: #d4d4d4; - background: none; -} - -/* Medium Devices, Desktops */ -@media only screen and (max-width : 992px) { - header { - width: 20%; - } - - .form-container { - width: 30%; - } - - .message p { - width: 70% - } -} - -/* Small Devices, Tablets */ -@media only screen and (max-width : 768px) { - header { - width: 30%; - } - - .form-container { - width: 50%; - } - - .message p { - width: 80% - } -} - -/* Extra Small Devices, Phones */ -@media only screen and (max-width : 480px) { - header { - width: 60%; - } - - .form-container { - width: 80%; - } - - .message p { - width: 85% - } -} diff --git a/web-ui/signup/images/pixelated-logo-orange.svg b/web-ui/signup/images/pixelated-logo-orange.svg deleted file mode 100644 index 7e0ef43d..00000000 --- a/web-ui/signup/images/pixelated-logo-orange.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/web-ui/signup/images/sent_email.png b/web-ui/signup/images/sent_email.png deleted file mode 100644 index ddaa11d0..00000000 Binary files a/web-ui/signup/images/sent_email.png and /dev/null differ diff --git a/web-ui/signup/index.html b/web-ui/signup/index.html deleted file mode 100644 index a1836721..00000000 --- a/web-ui/signup/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Pixelated Mail - - - - - -
-
-
- -
- - diff --git a/web-ui/signup/src/index.js b/web-ui/signup/src/index.js deleted file mode 100644 index 35de6407..00000000 --- a/web-ui/signup/src/index.js +++ /dev/null @@ -1,223 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import {createStore} from 'redux'; -import {Map} from 'immutable'; -import 'whatwg-fetch'; - - -class PixelatedComponent extends React.Component { - _updateStateFromStore() { - this.setState(this.props.store.getState().toJS()); - } - - componentWillMount() { - this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); - this._updateStateFromStore(); - } - - componentWillUnmount() { - this.unsubscribe() - } -} - - -class PixelatedForm extends PixelatedComponent { - _fetchAndDispatch(url, actionProperties) { - const immutableActionProperties = new Map(actionProperties); - this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); - fetch(url).then((response) => { - console.debug('got a reply', response); - return response.json() - }).then((json) => { - console.debug('got json', json); - setTimeout(() => { - this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); - }, 3000); - }).catch((error) => { - console.error('something went wrong', error); - this.props.store.dispatch(immutableActionProperties.merge({status: 'ERROR', error: error}).toJS()); - }); - } -} - - -class InviteCodeForm extends PixelatedForm { - render() { - return ( -
-
- - -
- -
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); - } -} - - -class CreateAccountForm extends PixelatedForm { - render() { - return ( -
- @domain.com -
- - -
- -
- - -
- - -
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT', username: event.target['username'].value, password: event.target['password'].value}); - } -} - - -class BackupEmailForm extends PixelatedForm { - render() { - return ( -
-
- - -
- - -

- I didn't receive anything. Send the email again -

-
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - this._fetchAndDispatch('dummy.json', {type: 'SUBMIT_BACKUP_EMAIL', backupEmail: event.target['backup-email'].value}); - } -} - - -class BackupEmailSentForm extends PixelatedForm { - render() { - return ( -
- {this.state.isFetching || I received the codes.
Go to my inbox
} -

- I didn't receive anything. Send the email again -

-
- ); - } - - _handleClick(event) { - event.stopPropagation(); - event.preventDefault(); - } -} - - -class SignUp extends PixelatedComponent { - render() { - return ( -
-
-

{this.state.header}

- {this.state.icon} -

{this.state.summary}

-
-
- {this._form()} -
-
- ); - } - - _form() { - switch(this.state.form) { - case 'invite_code': return ; - case 'create_account': return ; - case 'backup_email': return ; - case 'backup_email_sent': return ; - default: throw Error('TODO'); - } - } -} - - -const initialState = new Map({ - isFetching: false, - form: 'invite_code', - header: 'Welcome', - icon: null, - summary: ['Do you have an invite code?',
, 'Type it below'], -}); - - -const store = createStore((state=initialState, action) => { - switch (action.type) { - case 'SUBMIT_INVITE_CODE': - return state.merge({ - inviteCode: action.inviteCode, - form: 'create_account', - header: 'Create your account', - summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', - }); - case 'SUBMIT_CREATE_ACCOUNT': - return state.merge({ - username: action.username, - password: action.password, - form: 'backup_email', - header: 'In case you lose your password...', - summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', - }); - case 'SUBMIT_BACKUP_EMAIL': - switch (action.status) { - case 'STARTED': - return state.merge({ - isFetching: true, - backupEmail: action.backupEmail, - form: 'backup_email_sent', - icon:

, - summary: 'An email was sent to the email you provided. Check your spam folder, just in case.', - }); - case 'SUCCESS': - return state.merge({ - isFetching: false, - }); - case 'ERROR': - return state.merge({ - isFetching: false, - }); - default: - return state; - } - case 'SUBMIT_BACKUP_EMAIL_SENT': - return state.merge({}); - default: - return state; - } -}); - - -ReactDOM.render( - , - document.getElementById('app') -); diff --git a/web-ui/src/js/index.js b/web-ui/src/js/index.js new file mode 100644 index 00000000..35de6407 --- /dev/null +++ b/web-ui/src/js/index.js @@ -0,0 +1,223 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {createStore} from 'redux'; +import {Map} from 'immutable'; +import 'whatwg-fetch'; + + +class PixelatedComponent extends React.Component { + _updateStateFromStore() { + this.setState(this.props.store.getState().toJS()); + } + + componentWillMount() { + this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); + this._updateStateFromStore(); + } + + componentWillUnmount() { + this.unsubscribe() + } +} + + +class PixelatedForm extends PixelatedComponent { + _fetchAndDispatch(url, actionProperties) { + const immutableActionProperties = new Map(actionProperties); + this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); + fetch(url).then((response) => { + console.debug('got a reply', response); + return response.json() + }).then((json) => { + console.debug('got json', json); + setTimeout(() => { + this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); + }, 3000); + }).catch((error) => { + console.error('something went wrong', error); + this.props.store.dispatch(immutableActionProperties.merge({status: 'ERROR', error: error}).toJS()); + }); + } +} + + +class InviteCodeForm extends PixelatedForm { + render() { + return ( +
+
+ + +
+ +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); + } +} + + +class CreateAccountForm extends PixelatedForm { + render() { + return ( +
+ @domain.com +
+ + +
+ +
+ + +
+ + +
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT', username: event.target['username'].value, password: event.target['password'].value}); + } +} + + +class BackupEmailForm extends PixelatedForm { + render() { + return ( +
+
+ + +
+ + +

+ I didn't receive anything. Send the email again +

+
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + this._fetchAndDispatch('dummy.json', {type: 'SUBMIT_BACKUP_EMAIL', backupEmail: event.target['backup-email'].value}); + } +} + + +class BackupEmailSentForm extends PixelatedForm { + render() { + return ( +
+ {this.state.isFetching || I received the codes.
Go to my inbox
} +

+ I didn't receive anything. Send the email again +

+
+ ); + } + + _handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + } +} + + +class SignUp extends PixelatedComponent { + render() { + return ( +
+
+

{this.state.header}

+ {this.state.icon} +

{this.state.summary}

+
+
+ {this._form()} +
+
+ ); + } + + _form() { + switch(this.state.form) { + case 'invite_code': return ; + case 'create_account': return ; + case 'backup_email': return ; + case 'backup_email_sent': return ; + default: throw Error('TODO'); + } + } +} + + +const initialState = new Map({ + isFetching: false, + form: 'invite_code', + header: 'Welcome', + icon: null, + summary: ['Do you have an invite code?',
, 'Type it below'], +}); + + +const store = createStore((state=initialState, action) => { + switch (action.type) { + case 'SUBMIT_INVITE_CODE': + return state.merge({ + inviteCode: action.inviteCode, + form: 'create_account', + header: 'Create your account', + summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', + }); + case 'SUBMIT_CREATE_ACCOUNT': + return state.merge({ + username: action.username, + password: action.password, + form: 'backup_email', + header: 'In case you lose your password...', + summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', + }); + case 'SUBMIT_BACKUP_EMAIL': + switch (action.status) { + case 'STARTED': + return state.merge({ + isFetching: true, + backupEmail: action.backupEmail, + form: 'backup_email_sent', + icon:

, + summary: 'An email was sent to the email you provided. Check your spam folder, just in case.', + }); + case 'SUCCESS': + return state.merge({ + isFetching: false, + }); + case 'ERROR': + return state.merge({ + isFetching: false, + }); + default: + return state; + } + case 'SUBMIT_BACKUP_EMAIL_SENT': + return state.merge({}); + default: + return state; + } +}); + + +ReactDOM.render( + , + document.getElementById('app') +); -- cgit v1.2.3 From f3e4a7ddc4de74e118dd8573891751752b5e1172 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Fri, 18 Nov 2016 14:04:57 +0100 Subject: add logo and icon --- web-ui/public/images/pixelated-logo-orange.svg | 29 +++++++++++++++++++++++++ web-ui/public/images/sent_email.png | Bin 0 -> 9160 bytes 2 files changed, 29 insertions(+) create mode 100644 web-ui/public/images/pixelated-logo-orange.svg create mode 100644 web-ui/public/images/sent_email.png diff --git a/web-ui/public/images/pixelated-logo-orange.svg b/web-ui/public/images/pixelated-logo-orange.svg new file mode 100644 index 00000000..7e0ef43d --- /dev/null +++ b/web-ui/public/images/pixelated-logo-orange.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/web-ui/public/images/sent_email.png b/web-ui/public/images/sent_email.png new file mode 100644 index 00000000..ddaa11d0 Binary files /dev/null and b/web-ui/public/images/sent_email.png differ -- cgit v1.2.3 From 18bfeae8ec1e7e91248c90d5b76d2fe07598db9e Mon Sep 17 00:00:00 2001 From: Tayane Fernandes Date: Fri, 18 Nov 2016 11:59:34 -0200 Subject: [#801] Fix gitignore file to use more absolute paths --- .gitignore | 34 +++++++++++++++++----------------- web-ui/public/dummy.json | 1 + web-ui/signup/dummy.json | 1 - 3 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 web-ui/public/dummy.json delete mode 100644 web-ui/signup/dummy.json diff --git a/.gitignore b/.gitignore index 155686f0..ddaadaa5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,36 +3,36 @@ *.log *.DS_Store *.egg-info -web-ui/node_modules -web-ui/app/bower_components -web-ui/lib/ -web-ui/public/signup.js +/web-ui/node_modules +/web-ui/app/bower_components +/web-ui/lib/ +/web-ui/public/signup.js .tmp .sass-cache/ dist/ *archive.zip *.swp *.swo -web-ui/app/js/generated -web-ui/app/css +/web-ui/app/js/generated +/web-ui/app/css test-results.xml -control_tower.html -state.yml -.server.pid +/control_tower.html +/state.yml +*.pid *archive.zip artifacts/ -public/ +/public/ .DS_Store -screenshot* +/screenshot* *.pyc -env/ +/env/ .vagrant/ __pycache__/ .virtualenv # custom config file that can be used with the useragent /config -credentials.ini -pixelated.cfg -service/_trial_temp/ -_trial_temp -web-ui/coverage +/credentials.ini +/pixelated.cfg +/service/_trial_temp/ +/_trial_temp +/web-ui/coverage diff --git a/web-ui/public/dummy.json b/web-ui/public/dummy.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web-ui/public/dummy.json @@ -0,0 +1 @@ +{} diff --git a/web-ui/signup/dummy.json b/web-ui/signup/dummy.json deleted file mode 100644 index 0967ef42..00000000 --- a/web-ui/signup/dummy.json +++ /dev/null @@ -1 +0,0 @@ -{} -- cgit v1.2.3 From ae63265323f63e3287d59033748830c38d9e964e Mon Sep 17 00:00:00 2001 From: Tayane Fernandes Date: Fri, 18 Nov 2016 15:50:48 -0200 Subject: [#801] Add validation to the invite code Disable the submit button when the invite code is empty --- web-ui/src/js/index.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/web-ui/src/js/index.js b/web-ui/src/js/index.js index 35de6407..57ec42a6 100644 --- a/web-ui/src/js/index.js +++ b/web-ui/src/js/index.js @@ -26,10 +26,8 @@ class PixelatedForm extends PixelatedComponent { const immutableActionProperties = new Map(actionProperties); this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); fetch(url).then((response) => { - console.debug('got a reply', response); return response.json() }).then((json) => { - console.debug('got json', json); setTimeout(() => { this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); }, 3000); @@ -43,13 +41,19 @@ class PixelatedForm extends PixelatedComponent { class InviteCodeForm extends PixelatedForm { render() { + let className = "blue-button validation-link"; + + if(!this.state.inviteCodeValidation) { + className = className + " disabled"; + } + return (
- +
- +
); } @@ -59,6 +63,10 @@ class InviteCodeForm extends PixelatedForm { event.preventDefault(); this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); } + + _handleInputEmpty(event) { + this.props.store.dispatch({type: 'VALIDATE_INVITE_CODE', inviteCode: event.target.value}); + } } @@ -211,6 +219,10 @@ const store = createStore((state=initialState, action) => { } case 'SUBMIT_BACKUP_EMAIL_SENT': return state.merge({}); + case 'VALIDATE_INVITE_CODE': + return state.merge({ + inviteCodeValidation: Boolean(action.inviteCode) + }); default: return state; } -- cgit v1.2.3 From ad1e8d322e98c50793749e87e56ace9cccc0ef18 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Mon, 21 Nov 2016 18:57:26 +0100 Subject: add test stub for PixelatedAuthSessionWrapper --- service/test/unit/resources/test_auth.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 service/test/unit/resources/test_auth.py diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py new file mode 100644 index 00000000..5f65e199 --- /dev/null +++ b/service/test/unit/resources/test_auth.py @@ -0,0 +1,30 @@ +import unittest + +from mockito import mock + +from test.unit.resources import DummySite +from twisted.web.test.requesthelper import DummyRequest +from pixelated.resources.auth import PixelatedAuthSessionWrapper + + +class TestRootResource(unittest.TestCase): + + def setUp(self): + self.portal = mock() + self.mock_root_resource = mock() + self.anonymous_resource = mock() + self.credential_factories = mock() + + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.mock_root_resource, self.anonymous_resource, self.credential_factories) + self.web = DummySite(self.session_wrapper) + + def test_should_use_login_resource_when_the_user_is_not_logged_in (self): + request = DummyRequest(['']) + self.session_wrapper.getChildWithDefault('/', request) + + def assert_response(_): + self.assertEquals(len(matches), 1) + + d.addCallback(assert_response) + return d + -- cgit v1.2.3 From d5de22115b4d091469f1abad8d8b9ae7651caa3b Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 22 Nov 2016 16:16:20 +0100 Subject: fix first test for auth session wrapper --- service/test/unit/resources/test_auth.py | 44 +++++++++++++---------- service/test/unit/resources/test_root_resource.py | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 5f65e199..ac2529dd 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -1,30 +1,36 @@ -import unittest - -from mockito import mock - +from mockito import mock, when, any as ANY +from pixelated.resources.auth import PixelatedAuthSessionWrapper +from pixelated.resources.login_resource import LoginResource +from pixelated.resources.root_resource import RootResource from test.unit.resources import DummySite +from twisted.cred.checkers import ANONYMOUS +from twisted.internet.defer import succeed +from twisted.trial import unittest +from twisted.web.resource import IResource from twisted.web.test.requesthelper import DummyRequest -from pixelated.resources.auth import PixelatedAuthSessionWrapper -class TestRootResource(unittest.TestCase): +class TestPixelatedAuthSessionWrapper(unittest.TestCase): def setUp(self): - self.portal = mock() - self.mock_root_resource = mock() - self.anonymous_resource = mock() - self.credential_factories = mock() + self.portal_mock = mock() + self.root_resource_mock = mock() + self.anonymous_resource_mock = mock() + credential_factories_mock = mock() - self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.mock_root_resource, self.anonymous_resource, self.credential_factories) - self.web = DummySite(self.session_wrapper) + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource_mock, self.anonymous_resource_mock, credential_factories_mock) - def test_should_use_login_resource_when_the_user_is_not_logged_in (self): - request = DummyRequest(['']) - self.session_wrapper.getChildWithDefault('/', request) + def test_should_use_login_resource_when_the_user_is_not_logged_in(self): + request = DummyRequest([]) + request.prepath = [''] + request.path = '/' + when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, ANONYMOUS, lambda: None))) - def assert_response(_): - self.assertEquals(len(matches), 1) + deferred_resource = self.session_wrapper.getChildWithDefault('/', request) + d = deferred_resource.d - d.addCallback(assert_response) - return d + def assert_anonymous_resource(resource): + self.assertIs(resource, self.anonymous_resource_mock) + d.addCallback(assert_anonymous_resource) + return d diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 4ff11ce8..7a7b2005 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -1,4 +1,3 @@ -import unittest import re from mock import MagicMock, patch @@ -7,6 +6,7 @@ from mockito import mock, when, any as ANY from pixelated.application import UserAgentMode from pixelated.resources.features_resource import FeaturesResource from test.unit.resources import DummySite +from twisted.trial import unittest from twisted.web.test.requesthelper import DummyRequest from pixelated.resources.root_resource import RootResource, MODE_STARTUP, MODE_RUNNING -- cgit v1.2.3 From 68c2667fc568055c7bf6a676e1f12e61154fbab6 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 22 Nov 2016 16:20:53 +0100 Subject: add test for logged-in resource --- service/test/unit/resources/test_auth.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index ac2529dd..adff1083 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -14,19 +14,20 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): def setUp(self): self.portal_mock = mock() + self.user_uuid_mock = mock() self.root_resource_mock = mock() self.anonymous_resource_mock = mock() credential_factories_mock = mock() self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource_mock, self.anonymous_resource_mock, credential_factories_mock) + self.request = DummyRequest([]) + self.request.prepath = [''] + self.request.path = '/' - def test_should_use_login_resource_when_the_user_is_not_logged_in(self): - request = DummyRequest([]) - request.prepath = [''] - request.path = '/' + def test_should_proxy_to_login_resource_when_the_user_is_not_logged_in(self): when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, ANONYMOUS, lambda: None))) - deferred_resource = self.session_wrapper.getChildWithDefault('/', request) + deferred_resource = self.session_wrapper.getChildWithDefault('/', self.request) d = deferred_resource.d def assert_anonymous_resource(resource): @@ -34,3 +35,15 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): d.addCallback(assert_anonymous_resource) return d + + def test_should_proxy_to_root_resource_when_the_user_is_logged_in(self): + when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, self.user_uuid_mock, lambda: None))) + + deferred_resource = self.session_wrapper.getChildWithDefault('/', self.request) + d = deferred_resource.d + + def assert_root_resource(resource): + self.assertIs(resource, self.root_resource_mock) + + d.addCallback(assert_root_resource) + return d -- cgit v1.2.3 From 59644fafb501422295d430912d8711a7d11195b5 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 10:02:38 +0100 Subject: fix archive resource unit test --- service/test/unit/resources/test_archive_resource.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/service/test/unit/resources/test_archive_resource.py b/service/test/unit/resources/test_archive_resource.py index 28078222..186078a5 100644 --- a/service/test/unit/resources/test_archive_resource.py +++ b/service/test/unit/resources/test_archive_resource.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest import json from mockito import mock, when, verify from test.unit.resources import DummySite @@ -15,11 +15,16 @@ class TestArchiveResource(unittest.TestCase): def test_render_POST_should_archive_mails(self): request = DummyRequest(['/mails/archive']) request.method = 'POST' + idents = ['1', '2'] content = mock() when(content).read().thenReturn(json.dumps({'idents': ['1', '2']})) - when(self.mail_service).archive_mail('1').thenReturn(defer.Deferred()) - when(self.mail_service).archive_mail('2').thenReturn(defer.Deferred()) + d1 = defer.Deferred() + d1.callback(None) + when(self.mail_service).archive_mail('1').thenReturn(d1) + d2 = defer.Deferred() + d2.callback(None) + when(self.mail_service).archive_mail('2').thenReturn(d2) request.content = content d = self.web.get(request) -- cgit v1.2.3 From 20df0b4236b9939776bf15d955d36501566cb486 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 11:12:26 +0100 Subject: readability --- service/test/unit/resources/test_archive_resource.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/service/test/unit/resources/test_archive_resource.py b/service/test/unit/resources/test_archive_resource.py index 186078a5..1876f897 100644 --- a/service/test/unit/resources/test_archive_resource.py +++ b/service/test/unit/resources/test_archive_resource.py @@ -19,12 +19,8 @@ class TestArchiveResource(unittest.TestCase): content = mock() when(content).read().thenReturn(json.dumps({'idents': ['1', '2']})) - d1 = defer.Deferred() - d1.callback(None) - when(self.mail_service).archive_mail('1').thenReturn(d1) - d2 = defer.Deferred() - d2.callback(None) - when(self.mail_service).archive_mail('2').thenReturn(d2) + when(self.mail_service).archive_mail('1').thenReturn(defer.succeed(None)) + when(self.mail_service).archive_mail('2').thenReturn(defer.succeed(None)) request.content = content d = self.web.get(request) -- cgit v1.2.3 From 819d3b0c974fe1b937adbdc5205d7810b88faa4e Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 11:13:13 +0100 Subject: fix mails resource unit test --- service/test/unit/resources/test_mails_resource.py | 25 +++++++++------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/service/test/unit/resources/test_mails_resource.py b/service/test/unit/resources/test_mails_resource.py index 2d9cb33c..bdd15657 100644 --- a/service/test/unit/resources/test_mails_resource.py +++ b/service/test/unit/resources/test_mails_resource.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from mock import patch from mockito import mock, when, verify, any as ANY @@ -39,17 +39,16 @@ class TestMailsResource(unittest.TestCase): @patch('leap.common.events.register') def test_render_GET_should_unicode_mails_search_query(self, mock_register): - request = DummyRequest(['/mails']) + request = DummyRequest([]) non_unicode_search_term = 'coração' request.addArg('q', non_unicode_search_term) request.addArg('w', 25) request.addArg('p', 1) unicodified_search_term = u'coração' - when(self.mail_service).mails(unicodified_search_term, 25, 1).thenReturn(defer.Deferred()) + when(self.mail_service).mails(unicodified_search_term, 25, 1).thenReturn(defer.succeed(([], 0))) mails_resource = MailsResource(self.services_factory) - mails_resource.isLeaf = True web = DummySite(mails_resource) d = web.get(request) @@ -61,15 +60,13 @@ class TestMailsResource(unittest.TestCase): @patch('leap.common.events.register') def test_render_PUT_should_store_draft_with_attachments(self, mock_register): - request = DummyRequest(['/mails']) + request = DummyRequest([]) request.method = 'PUT' - content = mock() - when(content).read().thenReturn('{"attachments": [{"ident": "some fake attachment id"}]}') - when(self.mail_service).attachment('some fake attachment id').thenReturn(defer.Deferred()) - request.content = content + request.content = mock() + when(request.content).read().thenReturn('{"attachments": [{"ident": "some fake attachment id"}]}') + when(self.mail_service).attachment('some fake attachment id').thenReturn(defer.succeed({'content': mock()})) mails_resource = MailsResource(self.services_factory) - mails_resource.isLeaf = True web = DummySite(mails_resource) d = web.get(request) @@ -81,19 +78,17 @@ class TestMailsResource(unittest.TestCase): @patch('leap.common.events.register') def test_render_POST_should_send_email_with_attachments(self, mock_register): - request = DummyRequest(['/mails']) + request = DummyRequest([]) request.method = 'POST' - content = mock() - when(content).read().thenReturn('{"attachments": [{"ident": "some fake attachment id"}]}') + request.content = mock() + when(request.content).read().thenReturn('{"attachments": [{"ident": "some fake attachment id"}]}') when(self.mail_service).attachment('some fake attachment id').thenReturn(defer.succeed({"content": "some content"})) as_dictable = mock() when(as_dictable).as_dict().thenReturn({}) when(self.mail_service).send_mail({"attachments": [{"ident": "some fake attachment id", "raw": "some content"}]})\ .thenReturn(defer.succeed(as_dictable)) - request.content = content mails_resource = MailsResource(self.services_factory) - mails_resource.isLeaf = True web = DummySite(mails_resource) d = web.get(request) -- cgit v1.2.3 From ced565121604f9834f83ec485538bb2e2a0c9232 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 11:31:22 +0100 Subject: replace stdlib's unittest with trials's unittest for all unittest --- service/test/unit/adapter/mailstore/test_body_parser.py | 2 +- service/test/unit/adapter/search/test_search.py | 2 +- service/test/unit/adapter/test_contacts.py | 2 +- service/test/unit/adapter/test_draft_service.py | 2 +- service/test/unit/adapter/test_status.py | 2 +- service/test/unit/adapter/test_tag.py | 2 +- service/test/unit/bitmask_libraries/test_abstract_leap.py | 2 +- service/test/unit/bitmask_libraries/test_certs.py | 2 +- service/test/unit/bitmask_libraries/test_smtp_client_certificate.py | 2 +- service/test/unit/config/test_register.py | 2 +- service/test/unit/config/test_services.py | 2 +- service/test/unit/config/test_sessions.py | 1 + service/test/unit/config/test_site.py | 2 +- service/test/unit/maintenance/test_commands.py | 2 +- service/test/unit/resources/test_attachments_resource.py | 2 +- service/test/unit/resources/test_helpers.py | 2 +- service/test/unit/resources/test_sandbox_resource.py | 2 +- service/test/unit/resources/test_user_settings_resource.py | 2 +- service/test/unit/support/mail_generator_test.py | 2 +- service/test/unit/support/test_encrypted_file_storage.py | 2 +- service/test/unit/support/test_functional.py | 2 +- service/test/unit/support/test_markov.py | 2 +- service/test/unit/support/test_replier.py | 2 +- service/test/unit/test_application.py | 2 +- service/test/unit/test_welcome_mail.py | 2 +- 25 files changed, 25 insertions(+), 24 deletions(-) diff --git a/service/test/unit/adapter/mailstore/test_body_parser.py b/service/test/unit/adapter/mailstore/test_body_parser.py index 155b326c..cff0b09e 100644 --- a/service/test/unit/adapter/mailstore/test_body_parser.py +++ b/service/test/unit/adapter/mailstore/test_body_parser.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from mock import patch from pixelated.adapter.mailstore.body_parser import BodyParser diff --git a/service/test/unit/adapter/search/test_search.py b/service/test/unit/adapter/search/test_search.py index be37257c..1465961d 100644 --- a/service/test/unit/adapter/search/test_search.py +++ b/service/test/unit/adapter/search/test_search.py @@ -16,7 +16,7 @@ # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from pixelated.adapter.mailstore.leap_mailstore import LeapMail from pixelated.adapter.search import SearchEngine from tempdir import TempDir diff --git a/service/test/unit/adapter/test_contacts.py b/service/test/unit/adapter/test_contacts.py index 3510faf5..83383755 100644 --- a/service/test/unit/adapter/test_contacts.py +++ b/service/test/unit/adapter/test_contacts.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from pixelated.adapter.search.contacts import address_duplication_filter from pixelated.adapter.search.contacts import extract_mail_address diff --git a/service/test/unit/adapter/test_draft_service.py b/service/test/unit/adapter/test_draft_service.py index c2516013..e18589eb 100644 --- a/service/test/unit/adapter/test_draft_service.py +++ b/service/test/unit/adapter/test_draft_service.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from twisted.internet import defer from pixelated.adapter.mailstore.leap_mailstore import LeapMail diff --git a/service/test/unit/adapter/test_status.py b/service/test/unit/adapter/test_status.py index 5cd0fa1e..4624dcee 100644 --- a/service/test/unit/adapter/test_status.py +++ b/service/test/unit/adapter/test_status.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from pixelated.adapter.model.status import Status diff --git a/service/test/unit/adapter/test_tag.py b/service/test/unit/adapter/test_tag.py index a4fa819e..e6d2771d 100644 --- a/service/test/unit/adapter/test_tag.py +++ b/service/test/unit/adapter/test_tag.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from pixelated.adapter.model.tag import Tag diff --git a/service/test/unit/bitmask_libraries/test_abstract_leap.py b/service/test/unit/bitmask_libraries/test_abstract_leap.py index 237a1152..2fed2a4c 100644 --- a/service/test/unit/bitmask_libraries/test_abstract_leap.py +++ b/service/test/unit/bitmask_libraries/test_abstract_leap.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . import tempfile -import unittest +from twisted.trial import unittest from uuid import uuid4 import os diff --git a/service/test/unit/bitmask_libraries/test_certs.py b/service/test/unit/bitmask_libraries/test_certs.py index 9885759e..300830be 100644 --- a/service/test/unit/bitmask_libraries/test_certs.py +++ b/service/test/unit/bitmask_libraries/test_certs.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from pixelated.bitmask_libraries.certs import LeapCertificate from pixelated.config import leap_config diff --git a/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py b/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py index 241dcbae..1ed08653 100644 --- a/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py +++ b/service/test/unit/bitmask_libraries/test_smtp_client_certificate.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . import os -import unittest +from twisted.trial import unittest import tempdir import leap.common.certs as certs from mockito import mock, unstub, when, any as ANY diff --git a/service/test/unit/config/test_register.py b/service/test/unit/config/test_register.py index ca1e3a01..1d7918c4 100644 --- a/service/test/unit/config/test_register.py +++ b/service/test/unit/config/test_register.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from mock import patch, Mock from pixelated.register import validate_username, validate_password, _set_provider, register diff --git a/service/test/unit/config/test_services.py b/service/test/unit/config/test_services.py index ed221261..6361a3da 100644 --- a/service/test/unit/config/test_services.py +++ b/service/test/unit/config/test_services.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from mock import Mock, ANY, patch from mockito import mock, verify diff --git a/service/test/unit/config/test_sessions.py b/service/test/unit/config/test_sessions.py index 7ac6f8d1..5c5cf9be 100644 --- a/service/test/unit/config/test_sessions.py +++ b/service/test/unit/config/test_sessions.py @@ -53,6 +53,7 @@ class SessionTest(AbstractLeapTest): session.close() mail_fetcher_mock.stopService.assert_called_once() + @defer.inlineCallbacks def test_that_sync_defers_to_soledad(self): with patch('pixelated.config.sessions.reactor.callFromThread', new=_execute_func) as _: with patch('pixelated.config.sessions.LeapSession._create_incoming_mail_fetcher') as mail_fetcher_mock: diff --git a/service/test/unit/config/test_site.py b/service/test/unit/config/test_site.py index b8b23ef0..6911b4a5 100644 --- a/service/test/unit/config/test_site.py +++ b/service/test/unit/config/test_site.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from mockito import mock from pixelated.config.site import PixelatedSite from twisted.protocols.basic import LineReceiver diff --git a/service/test/unit/maintenance/test_commands.py b/service/test/unit/maintenance/test_commands.py index 812c1bc2..0a8ffe53 100644 --- a/service/test/unit/maintenance/test_commands.py +++ b/service/test/unit/maintenance/test_commands.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest import email from pixelated.maintenance import delete_all_mails, load_mails diff --git a/service/test/unit/resources/test_attachments_resource.py b/service/test/unit/resources/test_attachments_resource.py index 06ae765f..15f38406 100644 --- a/service/test/unit/resources/test_attachments_resource.py +++ b/service/test/unit/resources/test_attachments_resource.py @@ -1,5 +1,5 @@ import json -import unittest +from twisted.trial import unittest from mock import patch, MagicMock from mockito import mock, when, verify, any as ANY diff --git a/service/test/unit/resources/test_helpers.py b/service/test/unit/resources/test_helpers.py index a17ce755..25a52da2 100644 --- a/service/test/unit/resources/test_helpers.py +++ b/service/test/unit/resources/test_helpers.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest import re from pixelated.resources import respond_json, respond_json_deferred diff --git a/service/test/unit/resources/test_sandbox_resource.py b/service/test/unit/resources/test_sandbox_resource.py index 98b88b2d..4f263af1 100644 --- a/service/test/unit/resources/test_sandbox_resource.py +++ b/service/test/unit/resources/test_sandbox_resource.py @@ -1,5 +1,5 @@ import os -import unittest +from twisted.trial import unittest from twisted.internet import defer from twisted.web.test.requesthelper import DummyRequest diff --git a/service/test/unit/resources/test_user_settings_resource.py b/service/test/unit/resources/test_user_settings_resource.py index 30a3c4cd..e9748e72 100644 --- a/service/test/unit/resources/test_user_settings_resource.py +++ b/service/test/unit/resources/test_user_settings_resource.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest import json import ast diff --git a/service/test/unit/support/mail_generator_test.py b/service/test/unit/support/mail_generator_test.py index 9d604378..dd6da522 100644 --- a/service/test/unit/support/mail_generator_test.py +++ b/service/test/unit/support/mail_generator_test.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . from mailbox import mbox -import unittest +from twisted.trial import unittest import pkg_resources import random from mock import patch diff --git a/service/test/unit/support/test_encrypted_file_storage.py b/service/test/unit/support/test_encrypted_file_storage.py index 69b82f3d..8083430e 100644 --- a/service/test/unit/support/test_encrypted_file_storage.py +++ b/service/test/unit/support/test_encrypted_file_storage.py @@ -15,7 +15,7 @@ # along with Pixelated. If not, see . import os import shutil -import unittest +from twisted.trial import unittest from pixelated.support.encrypted_file_storage import EncryptedFileStorage diff --git a/service/test/unit/support/test_functional.py b/service/test/unit/support/test_functional.py index ad3cb16c..0b117032 100644 --- a/service/test/unit/support/test_functional.py +++ b/service/test/unit/support/test_functional.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Affero General Public License -import unittest +from twisted.trial import unittest from pixelated.support.functional import to_unicode diff --git a/service/test/unit/support/test_markov.py b/service/test/unit/support/test_markov.py index f0b0277d..911cef30 100644 --- a/service/test/unit/support/test_markov.py +++ b/service/test/unit/support/test_markov.py @@ -15,7 +15,7 @@ # along with Pixelated. If not, see . -import unittest +from twisted.trial import unittest from pixelated.support.markov import MarkovGenerator import random diff --git a/service/test/unit/support/test_replier.py b/service/test/unit/support/test_replier.py index 5e1c234a..ef9b321c 100644 --- a/service/test/unit/support/test_replier.py +++ b/service/test/unit/support/test_replier.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from pixelated.support import replier diff --git a/service/test/unit/test_application.py b/service/test/unit/test_application.py index 80d9ec14..67c044c2 100644 --- a/service/test/unit/test_application.py +++ b/service/test/unit/test_application.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from leap.common.events import catalog as events from mock import patch, MagicMock, ANY diff --git a/service/test/unit/test_welcome_mail.py b/service/test/unit/test_welcome_mail.py index 6462dceb..7eb65903 100644 --- a/service/test/unit/test_welcome_mail.py +++ b/service/test/unit/test_welcome_mail.py @@ -16,7 +16,7 @@ import os import re -import unittest +from twisted.trial import unittest from mockito import verify, mock from mockito.matchers import Matcher from email import message_from_file -- cgit v1.2.3 From 2a145e20398463d48e3756fb0e6edb2ce31e3f60 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 15:44:32 +0100 Subject: more verbose test output --- service/go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/service/go b/service/go index 0a2297f8..7df9111b 100755 --- a/service/go +++ b/service/go @@ -3,6 +3,7 @@ NUM_OF_CORES='' NUM_OF_JOBS='' TRIAL_PATH='' +TRIAL_REPORTER='verbose' function getTrialAbsolutePath { TRIAL_PATH="$(which trial)" @@ -58,14 +59,14 @@ function setupjs { function runIntegrationTests { echo "Executing Integration Tests." resolveNumOfJobs - trial -j $NUM_OF_JOBS --reporter=text $* test.integration + trial -j $NUM_OF_JOBS --reporter=$TRIAL_REPORTER $* test.integration echo "Done." } function runUnitTests { echo "Executing Unit Tests." removeZmqCertificates - trial --reporter=text $* test.unit + trial --reporter=$TRIAL_REPORTER $* test.unit echo "Done." } @@ -87,7 +88,7 @@ function runCoverageUnit { echo "Generating Unit Test Converage Information." coverage erase getTrialAbsolutePath - coverage run -p --source=pixelated $TRIAL_PATH --reporter=text $* test.unit + coverage run -p --source=pixelated $TRIAL_PATH --reporter=$TRIAL_REPORTER $* test.unit coverage combine coverage html echo "Done." @@ -97,9 +98,9 @@ function runCoverageIntegration { echo "Generating Integration Test Converage Information." coverage erase getTrialAbsolutePath - coverage run -p --source=pixelated $TRIAL_PATH --reporter=text $* test.integration + coverage run -p --source=pixelated $TRIAL_PATH --reporter=$TRIAL_REPORTER $* test.integration coverage combine - coverage html --ignore-errors + coverage html echo "Done." } @@ -107,8 +108,8 @@ function runCoverageUnitAndIntegration { echo "Generating Unit and Integration Test Converage Information." coverage erase getTrialAbsolutePath - coverage run -p --source=pixelated $TRIAL_PATH --reporter=text test.unit - coverage run -p --source=pixelated $TRIAL_PATH --reporter=text test.integration + coverage run -p --source=pixelated $TRIAL_PATH --reporter=$TRIAL_REPORTER test.unit + coverage run -p --source=pixelated $TRIAL_PATH --reporter=$TRIAL_REPORTER test.integration coverage combine coverage html echo "Done." -- cgit v1.2.3 From 8640658dca9c2a37f01922a127749af7eec7501e Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 16:52:05 +0100 Subject: remove an unnecessary patch --- service/test/unit/config/test_sessions.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/service/test/unit/config/test_sessions.py b/service/test/unit/config/test_sessions.py index 5c5cf9be..abae46d8 100644 --- a/service/test/unit/config/test_sessions.py +++ b/service/test/unit/config/test_sessions.py @@ -55,11 +55,10 @@ class SessionTest(AbstractLeapTest): @defer.inlineCallbacks def test_that_sync_defers_to_soledad(self): - with patch('pixelated.config.sessions.reactor.callFromThread', new=_execute_func) as _: - with patch('pixelated.config.sessions.LeapSession._create_incoming_mail_fetcher') as mail_fetcher_mock: - session = self._create_session() - yield session.sync() - self.soledad_session.sync.assert_called_once() + with patch('pixelated.config.sessions.LeapSession._create_incoming_mail_fetcher') as mail_fetcher_mock: + session = self._create_session() + yield session.sync() + self.soledad_session.sync.assert_called_once() def test_session_registers_to_generated_keys(self): email = 'someone@somedomain.tld' @@ -159,4 +158,6 @@ class SessionTest(AbstractLeapTest): def _execute_func(func): + print 'in _execute_func, before executing', func func() + print 'in _execute_func, after executing', func -- cgit v1.2.3 From 3d9d3a407de9e179d4f7be055a24c02fcf9bb418 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 17:40:24 +0100 Subject: remove another unnecessary patch --- service/test/unit/config/test_sessions.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/service/test/unit/config/test_sessions.py b/service/test/unit/config/test_sessions.py index abae46d8..b2aa2939 100644 --- a/service/test/unit/config/test_sessions.py +++ b/service/test/unit/config/test_sessions.py @@ -55,10 +55,9 @@ class SessionTest(AbstractLeapTest): @defer.inlineCallbacks def test_that_sync_defers_to_soledad(self): - with patch('pixelated.config.sessions.LeapSession._create_incoming_mail_fetcher') as mail_fetcher_mock: - session = self._create_session() - yield session.sync() - self.soledad_session.sync.assert_called_once() + session = self._create_session() + yield session.sync() + self.soledad_session.sync.assert_called_once() def test_session_registers_to_generated_keys(self): email = 'someone@somedomain.tld' @@ -158,6 +157,4 @@ class SessionTest(AbstractLeapTest): def _execute_func(func): - print 'in _execute_func, before executing', func func() - print 'in _execute_func, after executing', func -- cgit v1.2.3 From 2884195e96728c0432a8e8d0d2f747ae2baefd06 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 23 Nov 2016 17:59:31 +0100 Subject: mock out event registration for failing test in SnapCI --- service/test/unit/config/test_sessions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/test/unit/config/test_sessions.py b/service/test/unit/config/test_sessions.py index b2aa2939..98e5c69e 100644 --- a/service/test/unit/config/test_sessions.py +++ b/service/test/unit/config/test_sessions.py @@ -53,8 +53,9 @@ class SessionTest(AbstractLeapTest): session.close() mail_fetcher_mock.stopService.assert_called_once() + @patch('pixelated.config.sessions.register') @defer.inlineCallbacks - def test_that_sync_defers_to_soledad(self): + def test_that_sync_defers_to_soledad(self, *unused): session = self._create_session() yield session.sync() self.soledad_session.sync.assert_called_once() -- cgit v1.2.3 From c39921ef6ba7ed299a125b530b770b0e1ec16203 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 24 Nov 2016 10:56:59 +0100 Subject: add test for unauthorized resource --- service/test/unit/resources/test_auth.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index adff1083..80f1ebb0 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -3,9 +3,12 @@ from pixelated.resources.auth import PixelatedAuthSessionWrapper from pixelated.resources.login_resource import LoginResource from pixelated.resources.root_resource import RootResource from test.unit.resources import DummySite +from twisted.cred import error from twisted.cred.checkers import ANONYMOUS -from twisted.internet.defer import succeed +from twisted.internet.defer import succeed, fail +from twisted.python import failure from twisted.trial import unittest +from twisted.web._auth.wrapper import UnauthorizedResource from twisted.web.resource import IResource from twisted.web.test.requesthelper import DummyRequest @@ -47,3 +50,15 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): d.addCallback(assert_root_resource) return d + + def test_should_proxy_to_unauthorized_resource_when_login_fails(self): + when(self.portal_mock).login(ANY(), None, IResource).thenReturn(fail(failure.Failure(error.UnhandledCredentials('dummy message')))) + + deferred_resource = self.session_wrapper.getChildWithDefault('/', self.request) + d = deferred_resource.d + + def assert_unauthorized_resource(resource): + self.assertIsInstance(resource, UnauthorizedResource) + + d.addCallback(assert_unauthorized_resource) + return d -- cgit v1.2.3 From c2088d90c080eb56a0d6edd714ade80525e1ac00 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 24 Nov 2016 11:15:20 +0100 Subject: remove use of stdlib unittest from integration tests --- service/test/integration/test_feedback_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/test/integration/test_feedback_service.py b/service/test/integration/test_feedback_service.py index c50c1883..ff659396 100644 --- a/service/test/integration/test_feedback_service.py +++ b/service/test/integration/test_feedback_service.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from httmock import urlmatch, HTTMock from mockito import when from twisted.internet import defer -- cgit v1.2.3 From b00b7ff5d828099e8f0190fee44ad1daf2054717 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 24 Nov 2016 16:41:49 +0100 Subject: add public root resource to serve static files --- service/pixelated/resources/auth.py | 2 +- service/pixelated/resources/root_resource.py | 12 +++++++ service/test/unit/resources/test_root_resource.py | 44 ++++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/service/pixelated/resources/auth.py b/service/pixelated/resources/auth.py index adac985f..ef5c9710 100644 --- a/service/pixelated/resources/auth.py +++ b/service/pixelated/resources/auth.py @@ -108,7 +108,7 @@ class PixelatedAuthSessionWrapper(object): else: log.err( result, - "HTTPAuthSessionWrapper.getChildWithDefault encountered " + "PixelatedAuthSessionWrapper.getChildWithDefault encountered " "unexpected error") return ErrorPage(500, None, None) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 8fa80bb2..608e18ef 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -47,6 +47,18 @@ MODE_STARTUP = 1 MODE_RUNNING = 2 +class PublicRootResource(BaseResource, object): + + def __init__(self, services_factory, assets_path): + super(PublicRootResource, self).__init__(services_factory) + self._child_resources = dict( + assets=File(assets_path) + ) + + def getChild(self, path, request): + return self._child_resources.get(path) or NoResource() + + class RootResource(BaseResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 7a7b2005..082f2b22 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -1,14 +1,42 @@ +import os import re from mock import MagicMock, patch from mockito import mock, when, any as ANY +import pixelated from pixelated.application import UserAgentMode from pixelated.resources.features_resource import FeaturesResource from test.unit.resources import DummySite +from twisted.cred.checkers import ANONYMOUS +from twisted.internet.defer import succeed from twisted.trial import unittest +from twisted.web.resource import IResource +from twisted.web.static import File from twisted.web.test.requesthelper import DummyRequest -from pixelated.resources.root_resource import RootResource, MODE_STARTUP, MODE_RUNNING +from pixelated.resources.root_resource import PublicRootResource, RootResource, MODE_STARTUP, MODE_RUNNING + + +class TestPublicRootResource(unittest.TestCase): + + def setUp(self): + self.portal_mock = mock() + assets_path = os.path.abspath( + os.path.join(os.path.abspath(pixelated.__file__), '..', '..', '..', 'web-ui', 'public') + ) + services_factory = mock() + self.public_root_resource = PublicRootResource(services_factory, assets_path=assets_path) + self.web = DummySite(self.public_root_resource) + self.request = DummyRequest(['assets', 'dummy.json']) + + def test_assets_should_be_available(self): + d = self.web.get(self.request) + + def assert_response(_): + self.assertEqual(200, self.request.responseCode) + + d.addCallback(assert_response) + return d class TestRootResource(unittest.TestCase): @@ -103,6 +131,20 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d + def test_GET_should_return_404_for_non_existing_resource(self): + request = DummyRequest(['/non-existing-child']) + request.method = 'GET' + + request.getCookie = MagicMock(return_value='stubbed csrf token') + + d = self.web.get(request) + + def assert_not_found(_): + self.assertEqual(404, request.responseCode) + + d.addCallback(assert_not_found) + return d + def test_should_404_non_existing_resource_with_valid_csrf(self): request = DummyRequest(['/non-existing-child']) request.method = 'POST' -- cgit v1.2.3 From 7802cf70c3b2ec3c14fd735dc211b00914c731cb Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 24 Nov 2016 17:01:15 +0100 Subject: add login resource as child of public root resource --- service/pixelated/resources/root_resource.py | 5 +++-- service/test/unit/resources/test_root_resource.py | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 608e18ef..5477dca8 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -49,10 +49,11 @@ MODE_RUNNING = 2 class PublicRootResource(BaseResource, object): - def __init__(self, services_factory, assets_path): + def __init__(self, services_factory, assets_path, **kwargs): super(PublicRootResource, self).__init__(services_factory) self._child_resources = dict( - assets=File(assets_path) + assets=File(assets_path), + login=LoginResource(services_factory, **{k: kwargs[k] for k in kwargs if k in ('provider', 'disclaimer_banner', 'authenticator')}) ) def getChild(self, path, request): diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 082f2b22..9b3042a8 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -25,15 +25,25 @@ class TestPublicRootResource(unittest.TestCase): os.path.join(os.path.abspath(pixelated.__file__), '..', '..', '..', 'web-ui', 'public') ) services_factory = mock() - self.public_root_resource = PublicRootResource(services_factory, assets_path=assets_path) + self.public_root_resource = PublicRootResource(services_factory, assets_path=assets_path, provider=mock()) self.web = DummySite(self.public_root_resource) - self.request = DummyRequest(['assets', 'dummy.json']) def test_assets_should_be_available(self): - d = self.web.get(self.request) + request = DummyRequest(['assets', 'dummy.json']) + d = self.web.get(request) + + def assert_response(_): + self.assertEqual(200, request.responseCode) + + d.addCallback(assert_response) + return d + + def test_login_should_be_available(self): + request = DummyRequest(['login']) + d = self.web.get(request) def assert_response(_): - self.assertEqual(200, self.request.responseCode) + self.assertEqual(200, request.responseCode) d.addCallback(assert_response) return d -- cgit v1.2.3 From e313d7c8880192ab3261cdd8cb263f5eef28d40a Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Fri, 25 Nov 2016 15:50:50 +0100 Subject: make credentialsFactories parameter to auth session wrapper optional --- service/pixelated/application.py | 2 +- service/pixelated/resources/auth.py | 2 +- service/test/unit/resources/test_auth.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/service/pixelated/application.py b/service/pixelated/application.py index 46e5ba85..fa6568e6 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -159,7 +159,7 @@ def set_up_protected_resources(root_resource, provider, services_factory, banner _portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()]) anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator) - protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource, []) + protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource) root_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator) return protected_resource diff --git a/service/pixelated/resources/auth.py b/service/pixelated/resources/auth.py index ef5c9710..833c0f9d 100644 --- a/service/pixelated/resources/auth.py +++ b/service/pixelated/resources/auth.py @@ -75,7 +75,7 @@ class PixelatedAuthSessionWrapper(object): isLeaf = False - def __init__(self, portal, root_resource, anonymous_resource, credentialFactories): + def __init__(self, portal, root_resource, anonymous_resource, credentialFactories=[]): self._portal = portal self._credentialFactories = credentialFactories self._root_resource = root_resource diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 80f1ebb0..10650e53 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -20,9 +20,8 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): self.user_uuid_mock = mock() self.root_resource_mock = mock() self.anonymous_resource_mock = mock() - credential_factories_mock = mock() - self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource_mock, self.anonymous_resource_mock, credential_factories_mock) + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource_mock, self.anonymous_resource_mock) self.request = DummyRequest([]) self.request.prepath = [''] self.request.path = '/' -- cgit v1.2.3 From 8dbd0210911475ec48d23e741de192a09e23f101 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Fri, 25 Nov 2016 15:52:52 +0100 Subject: pass url *fragment* as path argument to getChildWithDefault --- service/test/unit/resources/test_auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 10650e53..05d07130 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -29,7 +29,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): def test_should_proxy_to_login_resource_when_the_user_is_not_logged_in(self): when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, ANONYMOUS, lambda: None))) - deferred_resource = self.session_wrapper.getChildWithDefault('/', self.request) + deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) d = deferred_resource.d def assert_anonymous_resource(resource): @@ -41,7 +41,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): def test_should_proxy_to_root_resource_when_the_user_is_logged_in(self): when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, self.user_uuid_mock, lambda: None))) - deferred_resource = self.session_wrapper.getChildWithDefault('/', self.request) + deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) d = deferred_resource.d def assert_root_resource(resource): @@ -53,7 +53,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): def test_should_proxy_to_unauthorized_resource_when_login_fails(self): when(self.portal_mock).login(ANY(), None, IResource).thenReturn(fail(failure.Failure(error.UnhandledCredentials('dummy message')))) - deferred_resource = self.session_wrapper.getChildWithDefault('/', self.request) + deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) d = deferred_resource.d def assert_unauthorized_resource(resource): -- cgit v1.2.3 From b97e47d564cc4bbd6b0f0ac2cccc0fa46490c764 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Fri, 25 Nov 2016 15:53:53 +0100 Subject: don't mock the root resource in auth wrapper test --- service/test/unit/resources/test_auth.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 05d07130..2b85a3cf 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -18,10 +18,11 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): def setUp(self): self.portal_mock = mock() self.user_uuid_mock = mock() - self.root_resource_mock = mock() + services_factory = mock() + self.root_resource = RootResource(services_factory) self.anonymous_resource_mock = mock() - self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource_mock, self.anonymous_resource_mock) + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource, self.anonymous_resource_mock) self.request = DummyRequest([]) self.request.prepath = [''] self.request.path = '/' @@ -45,7 +46,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): d = deferred_resource.d def assert_root_resource(resource): - self.assertIs(resource, self.root_resource_mock) + self.assertIs(resource, self.root_resource) d.addCallback(assert_root_resource) return d -- cgit v1.2.3 From 77cc41204e3cd8144187ad8cf50fffb3d00080f1 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Mon, 28 Nov 2016 12:00:06 +0100 Subject: split inbox resource out of root resource --- service/pixelated/resources/root_resource.py | 127 ++++++++++------------ service/test/unit/resources/test_root_resource.py | 107 +++++++++++------- 2 files changed, 127 insertions(+), 107 deletions(-) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 5477dca8..d35147f5 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -19,6 +19,7 @@ import os from string import Template from pixelated.resources.users import UsersResource +import pixelated from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableResource from pixelated.resources import IPixelatedSession from pixelated.resources.attachments_resource import AttachmentsResource @@ -47,44 +48,66 @@ MODE_STARTUP = 1 MODE_RUNNING = 2 -class PublicRootResource(BaseResource, object): +class InboxResource(BaseResource): + isLeaf = True - def __init__(self, services_factory, assets_path, **kwargs): - super(PublicRootResource, self).__init__(services_factory) - self._child_resources = dict( - assets=File(assets_path), - login=LoginResource(services_factory, **{k: kwargs[k] for k in kwargs if k in ('provider', 'disclaimer_banner', 'authenticator')}) - ) + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + self._templates_folder = self._get_templates_folder() + self._html_template = open(os.path.join(self._templates_folder, 'index.html')).read() + with open(os.path.join(self._templates_folder, 'Interstitial.html')) as f: + self.interstitial = f.read() + self._mode = MODE_STARTUP + + def initialize(self): + self._mode = MODE_RUNNING - def getChild(self, path, request): - return self._child_resources.get(path) or NoResource() + def _get_templates_folder(self): + path = os.path.dirname(os.path.abspath(pixelated.__file__)) + return os.path.join(path, 'assets') + + def _add_csrf_cookie(self, request): + csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + request.addCookie('XSRF-TOKEN', csrf_token) + + def _is_starting(self): + return self._mode == MODE_STARTUP + + def render_GET(self, request): + self._add_csrf_cookie(request) + if self._is_starting(): + return self.interstitial + else: + account_email = self.mail_service(request).account_email + response = Template(self._html_template).safe_substitute(account_email=account_email) + return str(response) class RootResource(BaseResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) + self._assets_folder = self._get_assets_folder() self._startup_assets_folder = self._get_startup_folder() - self._public_assets_folder = self._get_public_folder() self._static_folder = self._get_static_folder() self._html_template = open(os.path.join(self._static_folder, 'index.html')).read() self._services_factory = services_factory - self._child_resources = ChildResourcesMap() with open(os.path.join(self._startup_assets_folder, 'Interstitial.html')) as f: self.interstitial = f.read() + self._inbox_resource = InboxResource(services_factory) self._startup_mode() def _startup_mode(self): + self.putChild('assets', File(self._assets_folder)) self.putChild('startup-assets', File(self._startup_assets_folder)) - self.putChild('public-assets', File(self._public_assets_folder)) self._mode = MODE_STARTUP - def getChild(self, path, request): + def getChildWithDefault(self, path, request): if path == '': - return self + return self._inbox_resource if self._mode == MODE_STARTUP: return UnavailableResource() if self._is_xsrf_valid(request): - return self._child_resources.get(path) + return BaseResource.getChildWithDefault(self, path, request) return UnAuthorizedResource() def _is_xsrf_valid(self, request): @@ -103,40 +126,33 @@ class RootResource(BaseResource): return csrf_input and csrf_input == xsrf_token def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): - self._child_resources.add('sandbox', SandboxResource(self._static_folder)) - self._child_resources.add('assets', File(self._static_folder)) - self._child_resources.add('keys', KeysResource(self._services_factory)) - self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) - self._child_resources.add('contacts', ContactsResource(self._services_factory)) - self._child_resources.add('features', FeaturesResource(provider)) - self._child_resources.add('tags', TagsResource(self._services_factory)) - self._child_resources.add('mails', MailsResource(self._services_factory)) - self._child_resources.add('mail', MailResource(self._services_factory)) - self._child_resources.add('feedback', FeedbackResource(self._services_factory)) - self._child_resources.add('user-settings', UserSettingsResource(self._services_factory)) - self._child_resources.add('users', UsersResource(self._services_factory)) - self._child_resources.add(LoginResource.BASE_URL, - LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) - self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) - + self.putChild('sandbox', SandboxResource(self._static_folder)) + self.putChild('keys', KeysResource(self._services_factory)) + self.putChild(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) + self.putChild('contacts', ContactsResource(self._services_factory)) + self.putChild('features', FeaturesResource(provider)) + self.putChild('tags', TagsResource(self._services_factory)) + self.putChild('mails', MailsResource(self._services_factory)) + self.putChild('mail', MailResource(self._services_factory)) + self.putChild('feedback', FeedbackResource(self._services_factory)) + self.putChild('user-settings', UserSettingsResource(self._services_factory)) + self.putChild('users', UsersResource(self._services_factory)) + self.putChild(LoginResource.BASE_URL, + LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) + self.putChild(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) + + self._inbox_resource.initialize() self._mode = MODE_RUNNING + def _get_assets_folder(self): + pixelated_path = os.path.dirname(os.path.abspath(pixelated.__file__)) + return os.path.join(pixelated_path, '..', '..', 'web-ui', 'public') + # TODO: use the public folder for this def _get_startup_folder(self): path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(path, '..', 'assets') - def _get_public_folder(self): - public_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "public")) - # this is a workaround for packaging - if not os.path.exists(public_folder): - public_folder = os.path.abspath( - os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "public")) - if not os.path.exists(public_folder): - # TODO: how is this packaged? - public_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') - return public_folder - def _get_static_folder(self): static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) # this is a workaround for packaging @@ -146,30 +162,3 @@ class RootResource(BaseResource): if not os.path.exists(static_folder): static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') return static_folder - - def _is_starting(self): - return self._mode == MODE_STARTUP - - def _add_csrf_cookie(self, request): - csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() - request.addCookie('XSRF-TOKEN', csrf_token) - - def render_GET(self, request): - self._add_csrf_cookie(request) - if self._is_starting(): - return self.interstitial - else: - account_email = self.mail_service(request).account_email - response = Template(self._html_template).safe_substitute(account_email=account_email) - return str(response) - - -class ChildResourcesMap(object): - def __init__(self): - self._registry = {} - - def add(self, path, resource): - self._registry[path] = resource - - def get(self, path): - return self._registry.get(path) or NoResource() diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 9b3042a8..2c74d7b9 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -14,39 +14,7 @@ from twisted.trial import unittest from twisted.web.resource import IResource from twisted.web.static import File from twisted.web.test.requesthelper import DummyRequest -from pixelated.resources.root_resource import PublicRootResource, RootResource, MODE_STARTUP, MODE_RUNNING - - -class TestPublicRootResource(unittest.TestCase): - - def setUp(self): - self.portal_mock = mock() - assets_path = os.path.abspath( - os.path.join(os.path.abspath(pixelated.__file__), '..', '..', '..', 'web-ui', 'public') - ) - services_factory = mock() - self.public_root_resource = PublicRootResource(services_factory, assets_path=assets_path, provider=mock()) - self.web = DummySite(self.public_root_resource) - - def test_assets_should_be_available(self): - request = DummyRequest(['assets', 'dummy.json']) - d = self.web.get(request) - - def assert_response(_): - self.assertEqual(200, request.responseCode) - - d.addCallback(assert_response) - return d - - def test_login_should_be_available(self): - request = DummyRequest(['login']) - d = self.web.get(request) - - def assert_response(_): - self.assertEqual(200, request.responseCode) - - d.addCallback(assert_response) - return d +from pixelated.resources.root_resource import InboxResource, RootResource, MODE_STARTUP, MODE_RUNNING class TestRootResource(unittest.TestCase): @@ -63,12 +31,13 @@ class TestRootResource(unittest.TestCase): self.mail_service.account_email = self.MAIL_ADDRESS root_resource = RootResource(self.services_factory) - root_resource._html_template = "$account_email" - root_resource._mode = root_resource self.web = DummySite(root_resource) self.root_resource = root_resource def test_render_GET_should_template_account_email(self): + self.root_resource._inbox_resource._html_template = "$account_email" + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['']) request.addCookie = lambda key, value: 'stubbed' @@ -126,6 +95,8 @@ class TestRootResource(unittest.TestCase): request.requestHeaders.setRawHeaders('x-xsrf-token', [csrf_token]) def test_should_unauthorize_child_resource_ajax_requests_when_csrf_mismatch(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['/child']) request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') @@ -141,10 +112,25 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d + def test_GET_should_return_503_for_uninitialized_resource(self): + request = DummyRequest(['/sandbox/']) + request.method = 'GET' + + request.getCookie = MagicMock(return_value='stubbed csrf token') + + d = self.web.get(request) + + def assert_unavailable(_): + self.assertEqual(503, request.responseCode) + + d.addCallback(assert_unavailable) + return d + def test_GET_should_return_404_for_non_existing_resource(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['/non-existing-child']) request.method = 'GET' - request.getCookie = MagicMock(return_value='stubbed csrf token') d = self.web.get(request) @@ -156,10 +142,11 @@ class TestRootResource(unittest.TestCase): return d def test_should_404_non_existing_resource_with_valid_csrf(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['/non-existing-child']) request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') - request.getCookie = MagicMock(return_value='stubbed csrf token') d = self.web.get(request) @@ -175,7 +162,7 @@ class TestRootResource(unittest.TestCase): request = DummyRequest(['features']) request.getCookie = MagicMock(return_value='irrelevant -- stubbed') - self.root_resource._child_resources.add('features', FeaturesResource()) + self.root_resource.putChild('features', FeaturesResource()) self.root_resource._mode = MODE_RUNNING d = self.web.get(request) @@ -187,6 +174,8 @@ class TestRootResource(unittest.TestCase): return d def test_should_unauthorize_child_resource_non_ajax_POST_requests_when_csrf_input_mismatch(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['mails']) request.method = 'POST' request.addArg('csrftoken', 'some csrf token') @@ -204,3 +193,45 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d + + def test_assets_should_be_publicly_available(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + + request = DummyRequest(['assets', 'dummy.json']) + d = self.web.get(request) + + def assert_response(_): + self.assertEqual(200, request.responseCode) + + d.addCallback(assert_response) + return d + + def test_login_should_be_publicly_available(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + + request = DummyRequest(['login']) + d = self.web.get(request) + + def assert_response(_): + self.assertEqual(200, request.responseCode) + + d.addCallback(assert_response) + return d + + def test_root_should_be_handled_by_inbox_resource(self): + request = DummyRequest([]) + request.prepath = [''] + request.path = '/' + # TODO: setup mocked portal + + resource = self.root_resource.getChildWithDefault(request.prepath[-1], request) + self.assertIsInstance(resource, InboxResource) + + def test_inbox_should_not_be_public(self): + request = DummyRequest([]) + request.prepath = [''] + request.path = '/' + # TODO: setup mocked portal + + resource = self.root_resource.getChildWithDefault(request.prepath[-1], request) + self.assertIsInstance(resource, InboxResource) -- cgit v1.2.3 From b50db20c0a6603a3ea5f0b704baee1983fc34c1d Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 10:16:38 +0100 Subject: return resource instead of username/avatarId as avatar --- service/pixelated/resources/auth.py | 27 +++++++++++--------- service/test/unit/resources/test_auth.py | 43 ++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/service/pixelated/resources/auth.py b/service/pixelated/resources/auth.py index 833c0f9d..a2054f18 100644 --- a/service/pixelated/resources/auth.py +++ b/service/pixelated/resources/auth.py @@ -64,10 +64,18 @@ class SessionChecker(object): class PixelatedRealm(object): implements(portal.IRealm) + def __init__(self, authenticated_resource, public_resource): + self._authenticated_resource = authenticated_resource + self._public_resource = public_resource + def requestAvatar(self, avatarId, mind, *interfaces): - if IResource in interfaces: - return IResource, avatarId, lambda: None - raise NotImplementedError() + if IResource not in interfaces: + raise NotImplementedError() + if avatarId == checkers.ANONYMOUS: + avatar = self._public_resource + else: + avatar = self._authenticated_resource + return IResource, avatar, lambda: None @implementer(IResource) @@ -93,23 +101,18 @@ class PixelatedAuthSessionWrapper(object): return util.DeferredResource(self._login(creds, request)) def _login(self, credentials, request): - pattern = re.compile("^/sandbox/") - def loginSucceeded(args): interface, avatar, logout = args - if avatar == checkers.ANONYMOUS and not pattern.match(request.path): - return self._anonymous_resource - else: - return self._root_resource + # TODO: make sandbox public + return avatar def loginFailed(result): if result.check(error.Unauthorized, error.LoginFailed): return UnauthorizedResource(self._credentialFactories) else: - log.err( - result, + log.error( "PixelatedAuthSessionWrapper.getChildWithDefault encountered " - "unexpected error") + "unexpected error: %s" % result) return ErrorPage(500, None, None) d = self._portal.login(credentials, None, IResource) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 2b85a3cf..6bd0338a 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -1,34 +1,55 @@ from mockito import mock, when, any as ANY -from pixelated.resources.auth import PixelatedAuthSessionWrapper +from pixelated.resources.auth import SessionChecker, PixelatedRealm, PixelatedAuthSessionWrapper from pixelated.resources.login_resource import LoginResource from pixelated.resources.root_resource import RootResource from test.unit.resources import DummySite from twisted.cred import error -from twisted.cred.checkers import ANONYMOUS +from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess +from twisted.cred.portal import Portal from twisted.internet.defer import succeed, fail from twisted.python import failure from twisted.trial import unittest from twisted.web._auth.wrapper import UnauthorizedResource -from twisted.web.resource import IResource +from twisted.web.resource import IResource, getChildForRequest from twisted.web.test.requesthelper import DummyRequest +class TestPixelatedRealm(unittest.TestCase): + + def setUp(self): + self.authenticated_root_resource = mock() + self.public_root_resource = mock() + self.realm = PixelatedRealm(self.authenticated_root_resource, self.public_root_resource) + + def test_anonymous_user_gets_anonymous_resource(self): + interface, avatar, logout_handler = self.realm.requestAvatar(ANONYMOUS, None, IResource) + self.assertEqual(interface, IResource) + self.assertIs(avatar, self.public_root_resource) + + def test_authenticated_user_gets_root_resource(self): + interface, avatar, logout_handler = self.realm.requestAvatar('username', None, IResource) + self.assertEqual(interface, IResource) + self.assertIs(avatar, self.authenticated_root_resource) + + class TestPixelatedAuthSessionWrapper(unittest.TestCase): def setUp(self): - self.portal_mock = mock() - self.user_uuid_mock = mock() + self.realm_mock = mock() services_factory = mock() + session_checker = SessionChecker(services_factory) + self.portal = Portal(self.realm_mock, [session_checker, AllowAnonymousAccess()]) + self.user_uuid_mock = mock() self.root_resource = RootResource(services_factory) self.anonymous_resource_mock = mock() - self.session_wrapper = PixelatedAuthSessionWrapper(self.portal_mock, self.root_resource, self.anonymous_resource_mock) + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.root_resource, self.anonymous_resource_mock) self.request = DummyRequest([]) self.request.prepath = [''] self.request.path = '/' def test_should_proxy_to_login_resource_when_the_user_is_not_logged_in(self): - when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, ANONYMOUS, lambda: None))) + when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource_mock, lambda: None)) deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) d = deferred_resource.d @@ -40,7 +61,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): return d def test_should_proxy_to_root_resource_when_the_user_is_logged_in(self): - when(self.portal_mock).login(ANY(), None, IResource).thenReturn(succeed((IResource, self.user_uuid_mock, lambda: None))) + when(self.realm_mock).requestAvatar(ANY(), None, IResource).thenReturn((IResource, self.root_resource, lambda: None)) deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) d = deferred_resource.d @@ -51,14 +72,14 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): d.addCallback(assert_root_resource) return d - def test_should_proxy_to_unauthorized_resource_when_login_fails(self): - when(self.portal_mock).login(ANY(), None, IResource).thenReturn(fail(failure.Failure(error.UnhandledCredentials('dummy message')))) + def test_should_X_when_unauthenticated_user_requests_non_public_resource(self): + when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource_mock, lambda: None)) deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) d = deferred_resource.d def assert_unauthorized_resource(resource): - self.assertIsInstance(resource, UnauthorizedResource) + self.assertIs(resource, self.anonymous_resource_mock) d.addCallback(assert_unauthorized_resource) return d -- cgit v1.2.3 From 0bb7304f7cb87aed31f588bf40ae0a7fd949c2ba Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 11:10:26 +0100 Subject: move adding csrf to base resource --- service/pixelated/resources/__init__.py | 10 +++++- service/pixelated/resources/root_resource.py | 52 +++++----------------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py index 11611f0b..97346a6f 100644 --- a/service/pixelated/resources/__init__.py +++ b/service/pixelated/resources/__init__.py @@ -13,8 +13,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . - +import hashlib import json +import os from twisted.web.http import UNAUTHORIZED from twisted.web.resource import Resource @@ -26,6 +27,8 @@ from twisted.web.http import INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE log = Logger() +CSRF_TOKEN_LENGTH = 32 + class SetEncoder(json.JSONEncoder): def default(self, obj): @@ -62,6 +65,11 @@ class BaseResource(Resource): Resource.__init__(self) self._services_factory = services_factory + def _add_csrf_cookie(self, request): + csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + request.addCookie('XSRF-TOKEN', csrf_token) + log.debug('XSRF-TOKEN added: %s' % csrf_token) + def _get_user_id_from_request(self, request): if self._services_factory.mode.is_single_user: return None # it doesn't matter diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index d35147f5..e5b4227f 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -13,10 +13,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see . -import hashlib import json import os -from string import Template from pixelated.resources.users import UsersResource import pixelated @@ -34,56 +32,20 @@ from pixelated.resources.mail_resource import MailResource from pixelated.resources.mails_resource import MailsResource from pixelated.resources.tags_resource import TagsResource from pixelated.resources.keys_resource import KeysResource +from pixelated.resources.inbox_resource import InboxResource, MODE_STARTUP, MODE_RUNNING from twisted.web.resource import NoResource from twisted.web.static import File from twisted.logger import Logger -log = Logger() +logger = Logger() -CSRF_TOKEN_LENGTH = 32 +class PublicRootResource(BaseResource): + pass -MODE_STARTUP = 1 -MODE_RUNNING = 2 - -class InboxResource(BaseResource): - isLeaf = True - - def __init__(self, services_factory): - BaseResource.__init__(self, services_factory) - self._templates_folder = self._get_templates_folder() - self._html_template = open(os.path.join(self._templates_folder, 'index.html')).read() - with open(os.path.join(self._templates_folder, 'Interstitial.html')) as f: - self.interstitial = f.read() - self._mode = MODE_STARTUP - - def initialize(self): - self._mode = MODE_RUNNING - - def _get_templates_folder(self): - path = os.path.dirname(os.path.abspath(pixelated.__file__)) - return os.path.join(path, 'assets') - - def _add_csrf_cookie(self, request): - csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() - request.addCookie('XSRF-TOKEN', csrf_token) - - def _is_starting(self): - return self._mode == MODE_STARTUP - - def render_GET(self, request): - self._add_csrf_cookie(request) - if self._is_starting(): - return self.interstitial - else: - account_email = self.mail_service(request).account_email - response = Template(self._html_template).safe_substitute(account_email=account_email) - return str(response) - - -class RootResource(BaseResource): +class RootResource(PublicRootResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) self._assets_folder = self._get_assets_folder() @@ -100,6 +62,7 @@ class RootResource(BaseResource): self.putChild('assets', File(self._assets_folder)) self.putChild('startup-assets', File(self._startup_assets_folder)) self._mode = MODE_STARTUP + logger.debug('Root in STARTUP mode. %s' % self) def getChildWithDefault(self, path, request): if path == '': @@ -116,7 +79,9 @@ class RootResource(BaseResource): return True xsrf_token = request.getCookie('XSRF-TOKEN') + logger.debug('CSRF token: %s' % xsrf_token) + # TODO: how is comparing the cookie-csrf with the HTTP-header-csrf adding any csrf protection? ajax_request = (request.getHeader('x-requested-with') == 'XMLHttpRequest') if ajax_request: xsrf_header = request.getHeader('x-xsrf-token') @@ -143,6 +108,7 @@ class RootResource(BaseResource): self._inbox_resource.initialize() self._mode = MODE_RUNNING + logger.debug('Root in RUNNING mode. %s' % self) def _get_assets_folder(self): pixelated_path = os.path.dirname(os.path.abspath(pixelated.__file__)) -- cgit v1.2.3 From cec3cbf731f0e56cb96de27a070bdaf72e985eb0 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 11:40:21 +0100 Subject: root resource inherits from public root --- service/pixelated/resources/root_resource.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index e5b4227f..0788ffb1 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -42,12 +42,15 @@ logger = Logger() class PublicRootResource(BaseResource): - pass + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) class RootResource(PublicRootResource): + def __init__(self, services_factory): - BaseResource.__init__(self, services_factory) + PublicRootResource.__init__(self, services_factory) self._assets_folder = self._get_assets_folder() self._startup_assets_folder = self._get_startup_folder() self._static_folder = self._get_static_folder() -- cgit v1.2.3 From 1a770c015364cae30dca5995cf562d3c44e11a4c Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 15:10:19 +0100 Subject: pass resources to pixelated realm --- service/pixelated/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/pixelated/application.py b/service/pixelated/application.py index fa6568e6..8ec17bc5 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -155,10 +155,10 @@ def _setup_multi_user(args, root_resource, services_factory): def set_up_protected_resources(root_resource, provider, services_factory, banner=None, authenticator=None): session_checker = SessionChecker(services_factory) - realm = PixelatedRealm() + anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator) + realm = PixelatedRealm(root_resource, anonymous_resource) _portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()]) - anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator) protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource) root_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator) return protected_resource -- cgit v1.2.3 From 3f97f5c444ea4caa01111f3902871975430d9d97 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 17:34:41 +0100 Subject: add inbox resource --- service/pixelated/resources/inbox_resource.py | 64 +++++++++++++++++++ service/test/unit/resources/test_inbox_resource.py | 72 ++++++++++++++++++++++ service/test/unit/resources/test_root_resource.py | 19 ++---- 3 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 service/pixelated/resources/inbox_resource.py create mode 100644 service/test/unit/resources/test_inbox_resource.py diff --git a/service/pixelated/resources/inbox_resource.py b/service/pixelated/resources/inbox_resource.py new file mode 100644 index 00000000..47a3c072 --- /dev/null +++ b/service/pixelated/resources/inbox_resource.py @@ -0,0 +1,64 @@ +# +# 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 . +import hashlib +import os +from string import Template + +import pixelated +from pixelated.resources import BaseResource + +from twisted.logger import Logger + +logger = Logger() + + +MODE_STARTUP = 1 +MODE_RUNNING = 2 + + +class InboxResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + self._templates_folder = self._get_templates_folder() + self._html_template = open(os.path.join(self._templates_folder, 'index.html')).read() + with open(os.path.join(self._templates_folder, 'Interstitial.html')) as f: + self.interstitial = f.read() + self._mode = MODE_STARTUP + + def initialize(self): + self._mode = MODE_RUNNING + logger.debug('Inbox in RUNNING mode. %s' % self) + + def _get_templates_folder(self): + path = os.path.dirname(os.path.abspath(pixelated.__file__)) + return os.path.join(path, 'assets') + + def _is_starting(self): + return self._mode == MODE_STARTUP + + def render_GET(self, request): + logger.debug('Inbox rendering GET. %s' % self) + self._add_csrf_cookie(request) + if self._is_starting(): + logger.debug('Inbox rendering interstitial. %s' % self) + return self.interstitial + else: + logger.debug('Inbox rendering from template. %s' % self) + account_email = self.mail_service(request).account_email + response = Template(self._html_template).safe_substitute(account_email=account_email) + return str(response) diff --git a/service/test/unit/resources/test_inbox_resource.py b/service/test/unit/resources/test_inbox_resource.py new file mode 100644 index 00000000..03fe6f1a --- /dev/null +++ b/service/test/unit/resources/test_inbox_resource.py @@ -0,0 +1,72 @@ +import re + +from mock import MagicMock, patch +from mockito import mock, when, any as ANY + +from pixelated.application import UserAgentMode +from pixelated.resources.features_resource import FeaturesResource +from test.unit.resources import DummySite +from twisted.trial import unittest +from twisted.web.test.requesthelper import DummyRequest +from pixelated.resources.inbox_resource import InboxResource, MODE_STARTUP, MODE_RUNNING + + +class TestInboxResource(unittest.TestCase): + MAIL_ADDRESS = 'test_user@pixelated-project.org' + + def setUp(self): + mail_service = mock() + mail_service.account_email = self.MAIL_ADDRESS + + services = mock() + services.mail_service = mail_service + + services_factory = mock() + services_factory.mode = mock() + when(services_factory).services(ANY()).thenReturn(services) + + self.inbox_resource = InboxResource(services_factory) + self.web = DummySite(self.inbox_resource) + + def test_render_GET_should_template_account_email(self): + self.inbox_resource._html_template = "$account_email" + self.inbox_resource.initialize() + + request = DummyRequest(['']) + request.addCookie = lambda key, value: 'stubbed' + + d = self.web.get(request) + + def assert_response(_): + expected = "{0}".format(self.MAIL_ADDRESS) + matches = re.findall(expected, request.written[0]) + self.assertEquals(len(matches), 1) + + d.addCallback(assert_response) + return d + + def _test_should_renew_xsrf_cookie(self): + request = DummyRequest(['']) + request.addCookie = MagicMock() + generated_csrf_token = 'csrf_token' + mock_sha = MagicMock() + mock_sha.hexdigest = MagicMock(return_value=generated_csrf_token) + + with patch('hashlib.sha256', return_value=mock_sha): + d = self.web.get(request) + + def assert_csrf_cookie(_): + request.addCookie.assert_called_once_with('XSRF-TOKEN', generated_csrf_token) + + d.addCallback(assert_csrf_cookie) + return d + + # TODO should this be here or just in the root resource test? + def test_should_renew_xsrf_cookie_on_startup_mode(self): + self.inbox_resource._mode = MODE_STARTUP + self._test_should_renew_xsrf_cookie() + + # TODO should this be here or just in the root resource test? + def test_should_renew_xsrf_cookie_on_running_mode(self): + self.inbox_resource._mode = MODE_RUNNING + self._test_should_renew_xsrf_cookie() diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 2c74d7b9..079793b5 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -11,7 +11,7 @@ from test.unit.resources import DummySite from twisted.cred.checkers import ANONYMOUS from twisted.internet.defer import succeed from twisted.trial import unittest -from twisted.web.resource import IResource +from twisted.web.resource import IResource, getChildForRequest from twisted.web.static import File from twisted.web.test.requesthelper import DummyRequest from pixelated.resources.root_resource import InboxResource, RootResource, MODE_STARTUP, MODE_RUNNING @@ -34,22 +34,11 @@ class TestRootResource(unittest.TestCase): self.web = DummySite(root_resource) self.root_resource = root_resource - def test_render_GET_should_template_account_email(self): - self.root_resource._inbox_resource._html_template = "$account_email" - self.root_resource.initialize(provider=mock(), authenticator=mock()) - + def test_root_should_delegate_to_inbox(self): request = DummyRequest(['']) request.addCookie = lambda key, value: 'stubbed' - - d = self.web.get(request) - - def assert_response(_): - expected = "{0}".format(self.MAIL_ADDRESS) - matches = re.findall(expected, request.written[0]) - self.assertEquals(len(matches), 1) - - d.addCallback(assert_response) - return d + child_resource = getChildForRequest(self.root_resource, request) + self.assertIsInstance(child_resource, InboxResource) def _test_should_renew_xsrf_cookie(self): request = DummyRequest(['']) -- cgit v1.2.3 From 798858c79c0b10565f42365c6cdbf7d0549d0a2e Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 17:40:15 +0100 Subject: assert login url is delegated correctly --- service/test/unit/resources/test_root_resource.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 079793b5..8d658d7e 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -7,6 +7,7 @@ from mockito import mock, when, any as ANY import pixelated from pixelated.application import UserAgentMode from pixelated.resources.features_resource import FeaturesResource +from pixelated.resources.login_resource import LoginResource from test.unit.resources import DummySite from twisted.cred.checkers import ANONYMOUS from twisted.internet.defer import succeed @@ -14,7 +15,11 @@ from twisted.trial import unittest from twisted.web.resource import IResource, getChildForRequest from twisted.web.static import File from twisted.web.test.requesthelper import DummyRequest -from pixelated.resources.root_resource import InboxResource, RootResource, MODE_STARTUP, MODE_RUNNING +from pixelated.resources.root_resource import InboxResource, PublicRootResource, RootResource, MODE_STARTUP, MODE_RUNNING + + +class TestPublicRootResource(unittest.TestCase): + pass class TestRootResource(unittest.TestCase): @@ -34,12 +39,19 @@ class TestRootResource(unittest.TestCase): self.web = DummySite(root_resource) self.root_resource = root_resource - def test_root_should_delegate_to_inbox(self): + def test_root_url_should_delegate_to_inbox(self): request = DummyRequest(['']) request.addCookie = lambda key, value: 'stubbed' child_resource = getChildForRequest(self.root_resource, request) self.assertIsInstance(child_resource, InboxResource) + def test_login_url_should_delegate_to_login_resource(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['login']) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.root_resource, request) + self.assertIsInstance(child_resource, LoginResource) + def _test_should_renew_xsrf_cookie(self): request = DummyRequest(['']) request.addCookie = MagicMock() -- cgit v1.2.3 From 9b5d5a797c9f407183d1b9a6a2aea552a06c5ea1 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 17:47:07 +0100 Subject: make login resource part of the public root resource --- service/pixelated/resources/root_resource.py | 7 +++++-- service/test/unit/resources/test_root_resource.py | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 0788ffb1..24d097f9 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -46,6 +46,10 @@ class PublicRootResource(BaseResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) + def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): + self.putChild(LoginResource.BASE_URL, + LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) + class RootResource(PublicRootResource): @@ -94,6 +98,7 @@ class RootResource(PublicRootResource): return csrf_input and csrf_input == xsrf_token def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): + PublicRootResource.initialize(self, provider, disclaimer_banner, authenticator) self.putChild('sandbox', SandboxResource(self._static_folder)) self.putChild('keys', KeysResource(self._services_factory)) self.putChild(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) @@ -105,8 +110,6 @@ class RootResource(PublicRootResource): self.putChild('feedback', FeedbackResource(self._services_factory)) self.putChild('user-settings', UserSettingsResource(self._services_factory)) self.putChild('users', UsersResource(self._services_factory)) - self.putChild(LoginResource.BASE_URL, - LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) self.putChild(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) self._inbox_resource.initialize() diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 8d658d7e..06eaf1ad 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -19,7 +19,17 @@ from pixelated.resources.root_resource import InboxResource, PublicRootResource, class TestPublicRootResource(unittest.TestCase): - pass + + def setUp(self): + self.public_root_resource = PublicRootResource(mock()) + self.web = DummySite(self.public_root_resource) + + def test_login_url_should_delegate_to_login_resource(self): + self.public_root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['login']) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.public_root_resource, request) + self.assertIsInstance(child_resource, LoginResource) class TestRootResource(unittest.TestCase): @@ -35,9 +45,8 @@ class TestRootResource(unittest.TestCase): when(self.services_factory).services(ANY()).thenReturn(self.services) self.mail_service.account_email = self.MAIL_ADDRESS - root_resource = RootResource(self.services_factory) - self.web = DummySite(root_resource) - self.root_resource = root_resource + self.root_resource = RootResource(self.services_factory) + self.web = DummySite(self.root_resource) def test_root_url_should_delegate_to_inbox(self): request = DummyRequest(['']) -- cgit v1.2.3 From 6d82cddcb9a6f217dcb341e248124f00e613b48c Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 17:59:58 +0100 Subject: test root resource delegation on a bit higher level --- service/test/unit/resources/test_auth.py | 46 +++++++++++--------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 6bd0338a..793069dd 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -1,7 +1,7 @@ from mockito import mock, when, any as ANY from pixelated.resources.auth import SessionChecker, PixelatedRealm, PixelatedAuthSessionWrapper from pixelated.resources.login_resource import LoginResource -from pixelated.resources.root_resource import RootResource +from pixelated.resources.root_resource import PublicRootResource, RootResource from test.unit.resources import DummySite from twisted.cred import error from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess @@ -41,45 +41,31 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): self.portal = Portal(self.realm_mock, [session_checker, AllowAnonymousAccess()]) self.user_uuid_mock = mock() self.root_resource = RootResource(services_factory) - self.anonymous_resource_mock = mock() + self.anonymous_resource = PublicRootResource(services_factory) - self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.root_resource, self.anonymous_resource_mock) + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.root_resource, self.anonymous_resource) self.request = DummyRequest([]) self.request.prepath = [''] self.request.path = '/' - def test_should_proxy_to_login_resource_when_the_user_is_not_logged_in(self): - when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource_mock, lambda: None)) - - deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) + def test_root_url_should_delegate_to_public_root_resource_for_unauthenticated_user(self): + when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource, lambda: None)) + request = DummyRequest(['']) + deferred_resource = getChildForRequest(self.session_wrapper, request) d = deferred_resource.d - def assert_anonymous_resource(resource): - self.assertIs(resource, self.anonymous_resource_mock) + def assert_public_root_resource(resource): + self.assertIsInstance(resource, PublicRootResource) - d.addCallback(assert_anonymous_resource) - return d + return d.addCallback(assert_public_root_resource) - def test_should_proxy_to_root_resource_when_the_user_is_logged_in(self): + def test_root_url_should_delegate_to_protected_root_resource_for_authenticated_user(self): when(self.realm_mock).requestAvatar(ANY(), None, IResource).thenReturn((IResource, self.root_resource, lambda: None)) - - deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) - d = deferred_resource.d - - def assert_root_resource(resource): - self.assertIs(resource, self.root_resource) - - d.addCallback(assert_root_resource) - return d - - def test_should_X_when_unauthenticated_user_requests_non_public_resource(self): - when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource_mock, lambda: None)) - - deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) + request = DummyRequest(['']) + deferred_resource = getChildForRequest(self.session_wrapper, request) d = deferred_resource.d - def assert_unauthorized_resource(resource): - self.assertIs(resource, self.anonymous_resource_mock) + def assert_protected_root_resource(resource): + self.assertIsInstance(resource, RootResource) - d.addCallback(assert_unauthorized_resource) - return d + return d.addCallback(assert_protected_root_resource) -- cgit v1.2.3 From b785705033d70725eb979f54bb3c248c82d648af Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Tue, 29 Nov 2016 18:17:16 +0100 Subject: mock out usage or ZMQ --- service/test/unit/resources/test_root_resource.py | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 06eaf1ad..0db3bdbe 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -54,7 +54,8 @@ class TestRootResource(unittest.TestCase): child_resource = getChildForRequest(self.root_resource, request) self.assertIsInstance(child_resource, InboxResource) - def test_login_url_should_delegate_to_login_resource(self): + @patch('pixelated.config.sessions.register') + def test_login_url_should_delegate_to_login_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) request.addCookie = lambda key, value: 'stubbed' @@ -104,7 +105,8 @@ class TestRootResource(unittest.TestCase): request.requestHeaders.setRawHeaders('x-requested-with', ['XMLHttpRequest']) request.requestHeaders.setRawHeaders('x-xsrf-token', [csrf_token]) - def test_should_unauthorize_child_resource_ajax_requests_when_csrf_mismatch(self): + @patch('pixelated.config.sessions.register') + def test_should_unauthorize_child_resource_ajax_requests_when_csrf_mismatch(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['/child']) @@ -136,10 +138,11 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unavailable) return d - def test_GET_should_return_404_for_non_existing_resource(self): + @patch('pixelated.config.sessions.register') + def test_GET_should_return_404_for_non_existing_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) - request = DummyRequest(['/non-existing-child']) + request = DummyRequest(['non-existing-child']) request.method = 'GET' request.getCookie = MagicMock(return_value='stubbed csrf token') @@ -151,10 +154,11 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_not_found) return d - def test_should_404_non_existing_resource_with_valid_csrf(self): + @patch('pixelated.config.sessions.register') + def test_should_404_non_existing_resource_with_valid_csrf(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) - request = DummyRequest(['/non-existing-child']) + request = DummyRequest(['non-existing-child']) request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') request.getCookie = MagicMock(return_value='stubbed csrf token') @@ -183,7 +187,8 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d - def test_should_unauthorize_child_resource_non_ajax_POST_requests_when_csrf_input_mismatch(self): + @patch('pixelated.config.sessions.register') + def test_should_unauthorize_child_resource_non_ajax_POST_requests_when_csrf_input_mismatch(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['mails']) @@ -204,7 +209,8 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d - def test_assets_should_be_publicly_available(self): + @patch('pixelated.config.sessions.register') + def test_assets_should_be_publicly_available(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['assets', 'dummy.json']) @@ -216,7 +222,8 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_response) return d - def test_login_should_be_publicly_available(self): + @patch('pixelated.config.sessions.register') + def test_login_should_be_publicly_available(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) -- cgit v1.2.3 From 54600b0454809eeed12b01960a1d0ecaeb0d86a9 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 30 Nov 2016 10:01:20 +0100 Subject: mock out usage of ZMQ in the right place --- service/test/unit/resources/test_root_resource.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 0db3bdbe..443a00e6 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -54,7 +54,7 @@ class TestRootResource(unittest.TestCase): child_resource = getChildForRequest(self.root_resource, request) self.assertIsInstance(child_resource, InboxResource) - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_login_url_should_delegate_to_login_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) @@ -105,7 +105,7 @@ class TestRootResource(unittest.TestCase): request.requestHeaders.setRawHeaders('x-requested-with', ['XMLHttpRequest']) request.requestHeaders.setRawHeaders('x-xsrf-token', [csrf_token]) - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_should_unauthorize_child_resource_ajax_requests_when_csrf_mismatch(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) @@ -138,7 +138,7 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unavailable) return d - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_GET_should_return_404_for_non_existing_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) @@ -154,7 +154,7 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_not_found) return d - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_should_404_non_existing_resource_with_valid_csrf(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) @@ -187,7 +187,7 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_should_unauthorize_child_resource_non_ajax_POST_requests_when_csrf_input_mismatch(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) @@ -209,7 +209,7 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_assets_should_be_publicly_available(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) @@ -222,7 +222,7 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_response) return d - @patch('pixelated.config.sessions.register') + @patch('pixelated.resources.mails_resource.events.register') def test_login_should_be_publicly_available(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) -- cgit v1.2.3 From c10c6fb76f06e0cfc6f061a1bd9df14d689fb176 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 30 Nov 2016 10:29:27 +0100 Subject: redirect to login from root url when not logged in --- service/pixelated/resources/root_resource.py | 7 +++++++ service/test/unit/resources/test_root_resource.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 24d097f9..035d5f18 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -35,6 +35,7 @@ from pixelated.resources.keys_resource import KeysResource from pixelated.resources.inbox_resource import InboxResource, MODE_STARTUP, MODE_RUNNING from twisted.web.resource import NoResource from twisted.web.static import File +from twisted.web.util import Redirect from twisted.logger import Logger @@ -45,11 +46,17 @@ class PublicRootResource(BaseResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) + self._redirect_to_inbox_resource = Redirect('login') def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): self.putChild(LoginResource.BASE_URL, LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) + def getChildWithDefault(self, path, request): + if path == '': + return self._redirect_to_inbox_resource + return BaseResource.getChildWithDefault(self, path, request) + class RootResource(PublicRootResource): diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 443a00e6..1543f650 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -31,6 +31,19 @@ class TestPublicRootResource(unittest.TestCase): child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, LoginResource) + def test_root_url_should_redirect_to_login_resource(self): + self.public_root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['']) + request.addCookie = lambda key, value: 'stubbed' + d = self.web.get(request) + + def assert_redirect(request): + self.assertEqual(302, request.responseCode) + self.assertEqual(["login"], request.responseHeaders.getRawHeaders('location', [None])) + + d.addCallback(assert_redirect) + return d + class TestRootResource(unittest.TestCase): MAIL_ADDRESS = 'test_user@pixelated-project.org' -- cgit v1.2.3 From 4606a370f3abe73398097bb5ead898ee351e4a7a Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 30 Nov 2016 10:41:41 +0100 Subject: use PublicRootResource for root url instead of LoginResource --- service/pixelated/application.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/pixelated/application.py b/service/pixelated/application.py index 8ec17bc5..aee84cef 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -37,8 +37,7 @@ from pixelated.config.leap import initialize_leap_single_user, init_monkeypatche from pixelated.config.services import ServicesFactory, SingleUserServicesFactory from pixelated.config.site import PixelatedSite from pixelated.resources.auth import PixelatedRealm, PixelatedAuthSessionWrapper, SessionChecker -from pixelated.resources.login_resource import LoginResource -from pixelated.resources.root_resource import RootResource +from pixelated.resources.root_resource import PublicRootResource, RootResource log = Logger() @@ -155,11 +154,12 @@ def _setup_multi_user(args, root_resource, services_factory): def set_up_protected_resources(root_resource, provider, services_factory, banner=None, authenticator=None): session_checker = SessionChecker(services_factory) - anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator) + anonymous_resource = PublicRootResource(services_factory) realm = PixelatedRealm(root_resource, anonymous_resource) _portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()]) protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource) + anonymous_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator) root_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator) return protected_resource -- cgit v1.2.3 From 41f0886aeac43387dc8b4d54b1ca69f21e2ec2a8 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 30 Nov 2016 15:00:58 +0100 Subject: remove PublicRootResource and use a flag on RootResource instead --- service/pixelated/application.py | 4 +- service/pixelated/resources/root_resource.py | 62 ++++++++++------------ service/test/unit/resources/test_auth.py | 6 +-- service/test/unit/resources/test_root_resource.py | 63 ++++++++++++++++++++++- 4 files changed, 94 insertions(+), 41 deletions(-) diff --git a/service/pixelated/application.py b/service/pixelated/application.py index aee84cef..9f33cb82 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -37,7 +37,7 @@ from pixelated.config.leap import initialize_leap_single_user, init_monkeypatche from pixelated.config.services import ServicesFactory, SingleUserServicesFactory from pixelated.config.site import PixelatedSite from pixelated.resources.auth import PixelatedRealm, PixelatedAuthSessionWrapper, SessionChecker -from pixelated.resources.root_resource import PublicRootResource, RootResource +from pixelated.resources.root_resource import RootResource log = Logger() @@ -154,7 +154,7 @@ def _setup_multi_user(args, root_resource, services_factory): def set_up_protected_resources(root_resource, provider, services_factory, banner=None, authenticator=None): session_checker = SessionChecker(services_factory) - anonymous_resource = PublicRootResource(services_factory) + anonymous_resource = RootResource(services_factory, public=True) realm = PixelatedRealm(root_resource, anonymous_resource) _portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()]) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 035d5f18..3f09848f 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -42,26 +42,11 @@ from twisted.logger import Logger logger = Logger() -class PublicRootResource(BaseResource): +class RootResource(BaseResource): - def __init__(self, services_factory): + def __init__(self, services_factory, public=False): BaseResource.__init__(self, services_factory) - self._redirect_to_inbox_resource = Redirect('login') - - def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): - self.putChild(LoginResource.BASE_URL, - LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) - - def getChildWithDefault(self, path, request): - if path == '': - return self._redirect_to_inbox_resource - return BaseResource.getChildWithDefault(self, path, request) - - -class RootResource(PublicRootResource): - - def __init__(self, services_factory): - PublicRootResource.__init__(self, services_factory) + self._public = public self._assets_folder = self._get_assets_folder() self._startup_assets_folder = self._get_startup_folder() self._static_folder = self._get_static_folder() @@ -69,18 +54,19 @@ class RootResource(PublicRootResource): self._services_factory = services_factory with open(os.path.join(self._startup_assets_folder, 'Interstitial.html')) as f: self.interstitial = f.read() + self._redirect_to_login_resource = Redirect('login') self._inbox_resource = InboxResource(services_factory) self._startup_mode() def _startup_mode(self): - self.putChild('assets', File(self._assets_folder)) - self.putChild('startup-assets', File(self._startup_assets_folder)) + self.putChildProtected('assets', File(self._assets_folder)) + self.putChildPublic('startup-assets', File(self._startup_assets_folder)) self._mode = MODE_STARTUP logger.debug('Root in STARTUP mode. %s' % self) def getChildWithDefault(self, path, request): if path == '': - return self._inbox_resource + return self._redirect_to_login_resource if self._public else self._inbox_resource if self._mode == MODE_STARTUP: return UnavailableResource() if self._is_xsrf_valid(request): @@ -104,20 +90,28 @@ class RootResource(PublicRootResource): csrf_input = request.args.get('csrftoken', [None])[0] or json.loads(request.content.read()).get('csrftoken', [None])[0] return csrf_input and csrf_input == xsrf_token + def putChildPublic(self, path, resource): + return BaseResource.putChild(self, path, resource) + + def putChildProtected(self, path, resource): + return BaseResource.putChild(self, path, UnAuthorizedResource() if self._public else resource) + putChild = putChildProtected + def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): - PublicRootResource.initialize(self, provider, disclaimer_banner, authenticator) - self.putChild('sandbox', SandboxResource(self._static_folder)) - self.putChild('keys', KeysResource(self._services_factory)) - self.putChild(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) - self.putChild('contacts', ContactsResource(self._services_factory)) - self.putChild('features', FeaturesResource(provider)) - self.putChild('tags', TagsResource(self._services_factory)) - self.putChild('mails', MailsResource(self._services_factory)) - self.putChild('mail', MailResource(self._services_factory)) - self.putChild('feedback', FeedbackResource(self._services_factory)) - self.putChild('user-settings', UserSettingsResource(self._services_factory)) - self.putChild('users', UsersResource(self._services_factory)) - self.putChild(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) + self.putChildProtected('sandbox', SandboxResource(self._static_folder)) + self.putChildProtected('keys', KeysResource(self._services_factory)) + self.putChildProtected(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) + self.putChildProtected('contacts', ContactsResource(self._services_factory)) + self.putChildProtected('features', FeaturesResource(provider)) + self.putChildProtected('tags', TagsResource(self._services_factory)) + self.putChildProtected('mails', MailsResource(self._services_factory)) + self.putChildProtected('mail', MailResource(self._services_factory)) + self.putChildProtected('feedback', FeedbackResource(self._services_factory)) + self.putChildProtected('user-settings', UserSettingsResource(self._services_factory)) + self.putChildProtected('users', UsersResource(self._services_factory)) + self.putChildPublic(LoginResource.BASE_URL, + LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) + self.putChildProtected(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) self._inbox_resource.initialize() self._mode = MODE_RUNNING diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index 793069dd..f4012b1b 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -1,7 +1,7 @@ from mockito import mock, when, any as ANY from pixelated.resources.auth import SessionChecker, PixelatedRealm, PixelatedAuthSessionWrapper from pixelated.resources.login_resource import LoginResource -from pixelated.resources.root_resource import PublicRootResource, RootResource +from pixelated.resources.root_resource import RootResource from test.unit.resources import DummySite from twisted.cred import error from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess @@ -41,7 +41,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): self.portal = Portal(self.realm_mock, [session_checker, AllowAnonymousAccess()]) self.user_uuid_mock = mock() self.root_resource = RootResource(services_factory) - self.anonymous_resource = PublicRootResource(services_factory) + self.anonymous_resource = RootResource(services_factory, public=True) self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.root_resource, self.anonymous_resource) self.request = DummyRequest([]) @@ -55,7 +55,7 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): d = deferred_resource.d def assert_public_root_resource(resource): - self.assertIsInstance(resource, PublicRootResource) + self.assertIs(resource, self.anonymous_resource) return d.addCallback(assert_public_root_resource) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 1543f650..b674103c 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -6,6 +6,7 @@ from mockito import mock, when, any as ANY import pixelated from pixelated.application import UserAgentMode +from pixelated.resources import UnAuthorizedResource from pixelated.resources.features_resource import FeaturesResource from pixelated.resources.login_resource import LoginResource from test.unit.resources import DummySite @@ -15,15 +16,55 @@ from twisted.trial import unittest from twisted.web.resource import IResource, getChildForRequest from twisted.web.static import File from twisted.web.test.requesthelper import DummyRequest -from pixelated.resources.root_resource import InboxResource, PublicRootResource, RootResource, MODE_STARTUP, MODE_RUNNING +from pixelated.resources.root_resource import InboxResource, RootResource, MODE_STARTUP, MODE_RUNNING class TestPublicRootResource(unittest.TestCase): def setUp(self): - self.public_root_resource = PublicRootResource(mock()) + self.public_root_resource = RootResource(mock(), public=True) self.web = DummySite(self.public_root_resource) + def test_put_child_public_adds_resource(self): + self.public_root_resource.initialize(provider=mock(), authenticator=mock()) + url_fragment, resource_mock = 'some-url-fragment', mock() + self.public_root_resource.putChildPublic(url_fragment, resource_mock) + request = DummyRequest([url_fragment]) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.public_root_resource, request) + self.assertIs(child_resource, resource_mock) + + def test_put_child_protected_adds_unauthorized(self): + self.public_root_resource.initialize(provider=mock(), authenticator=mock()) + url_fragment, resource_mock = 'some-url-fragment', mock() + self.public_root_resource.putChildProtected(url_fragment, resource_mock) + request = DummyRequest([url_fragment]) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.public_root_resource, request) + self.assertIsInstance(child_resource, UnAuthorizedResource) + + def test_put_child_adds_unauthorized(self): + self.public_root_resource.initialize(provider=mock(), authenticator=mock()) + url_fragment, resource_mock = 'some-url-fragment', mock() + self.public_root_resource.putChild(url_fragment, resource_mock) + request = DummyRequest([url_fragment]) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.public_root_resource, request) + self.assertIsInstance(child_resource, UnAuthorizedResource) + + def test_private_resource_returns_401(self): + self.public_root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['mails']) + request.addCookie = lambda key, value: 'stubbed' + d = self.web.get(request) + + def assert_unauthorized(request): + self.assertEqual(401, request.responseCode) + self.assertEqual("Unauthorized!", request.written[0]) + + d.addCallback(assert_unauthorized) + return d + def test_login_url_should_delegate_to_login_resource(self): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) @@ -61,6 +102,24 @@ class TestRootResource(unittest.TestCase): self.root_resource = RootResource(self.services_factory) self.web = DummySite(self.root_resource) + def test_put_child_protected_adds_resource(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + url_fragment, resource_mock = 'some-url-fragment', mock() + self.root_resource.putChildProtected(url_fragment, resource_mock) + request = DummyRequest([url_fragment]) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.root_resource, request) + self.assertIs(child_resource, resource_mock) + + def test_put_child_adds_resource(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + url_fragment, resource_mock = 'some-url-fragment', mock() + self.root_resource.putChild(url_fragment, resource_mock) + request = DummyRequest([url_fragment]) + request.addCookie = lambda key, value: 'stubbed' + child_resource = getChildForRequest(self.root_resource, request) + self.assertIs(child_resource, resource_mock) + def test_root_url_should_delegate_to_inbox(self): request = DummyRequest(['']) request.addCookie = lambda key, value: 'stubbed' -- cgit v1.2.3 From a493da72d53fe90d679d7fa1980dd185415d9be3 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 30 Nov 2016 15:07:52 +0100 Subject: log a warnin when root child is not explicitly public/protected --- service/pixelated/resources/root_resource.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 3f09848f..7d5b0b0a 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -95,7 +95,10 @@ class RootResource(BaseResource): def putChildProtected(self, path, resource): return BaseResource.putChild(self, path, UnAuthorizedResource() if self._public else resource) - putChild = putChildProtected + + def putChild(self, path, resource): + logger.warn('Use either `putChildPublic` or `putChildProtected` on this resource') + return self.putChildProtected(path, resource) # to be on the safe side def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): self.putChildProtected('sandbox', SandboxResource(self._static_folder)) -- cgit v1.2.3 From 13378255c02b97184132881599ed47826963f54a Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Wed, 30 Nov 2016 16:11:27 +0100 Subject: add csrf token to login form --- service/pixelated/assets/login.html | 1 + service/pixelated/resources/login_resource.py | 6 ++++++ service/pixelated/resources/session.py | 10 +++++++++ service/test/unit/resources/test_login_resource.py | 13 +++++++++++ service/test/unit/resources/test_session.py | 25 ++++++++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 service/test/unit/resources/test_session.py diff --git a/service/pixelated/assets/login.html b/service/pixelated/assets/login.html index ff103f03..c2f5e78e 100644 --- a/service/pixelated/assets/login.html +++ b/service/pixelated/assets/login.html @@ -18,6 +18,7 @@
+ . +import hashlib +import os from zope.interface import Interface, Attribute, implements from twisted.python.components import registerAdapter from twisted.web.server import Session +CSRF_TOKEN_LENGTH = 32 + class IPixelatedSession(Interface): user_uuid = Attribute('The uuid of the currently logged in user') @@ -28,6 +32,7 @@ class PixelatedSession(object): def __init__(self, session): self.user_uuid = None + self._csrf_token = None def is_logged_in(self): return self.user_uuid is not None @@ -35,5 +40,10 @@ class PixelatedSession(object): def expire(self): self.user_uuid = None + def get_csrf_token(self): + if self._csrf_token is None: + self._csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + return self._csrf_token + registerAdapter(PixelatedSession, Session, IPixelatedSession) diff --git a/service/test/unit/resources/test_login_resource.py b/service/test/unit/resources/test_login_resource.py index d3d7ba64..696b0c46 100644 --- a/service/test/unit/resources/test_login_resource.py +++ b/service/test/unit/resources/test_login_resource.py @@ -157,6 +157,19 @@ class TestLoginResource(unittest.TestCase): d.addCallback(assert_default_invalid_banner_disclaimer_rendered) return d + def test_form_should_contain_csrftoken_input(self): + request = DummyRequest(['']) + + d = self.web.get(request) + + def assert_form_has_csrftoken_input(_): + input_username = 'name="csrftoken"' + written_response = ''.join(request.written) + self.assertIn(input_username, written_response) + + d.addCallback(assert_form_has_csrftoken_input) + return d + class TestLoginPOST(unittest.TestCase): def setUp(self): diff --git a/service/test/unit/resources/test_session.py b/service/test/unit/resources/test_session.py new file mode 100644 index 00000000..fe47483d --- /dev/null +++ b/service/test/unit/resources/test_session.py @@ -0,0 +1,25 @@ +from twisted.trial import unittest +from mockito import mock +from pixelated.resources.session import CSRF_TOKEN_LENGTH, PixelatedSession + + +class TestPixelatedSession(unittest.TestCase): + + def setUp(self): + self.pixelated_session = PixelatedSession(mock()) + + def test_csrf_token_should_be_configured_length(self): + self.assertEqual(len(self.pixelated_session.get_csrf_token()), 2 * CSRF_TOKEN_LENGTH) + + def test_csrf_token_should_be_hexdigested(self): + self.assertTrue(all(c in '0123456789abcdef' for c in self.pixelated_session.get_csrf_token())) + + def test_csrf_token_should_always_be_the_same_for_one_session(self): + first_csrf_token = self.pixelated_session.get_csrf_token() + second_csrf_token = self.pixelated_session.get_csrf_token() + self.assertEqual(first_csrf_token, second_csrf_token) + + def test_csrf_token_should_be_different_for_different_session(self): + first_csrf_token = self.pixelated_session.get_csrf_token() + second_csrf_token = PixelatedSession(mock()).get_csrf_token() + self.assertNotEqual(first_csrf_token, second_csrf_token) -- cgit v1.2.3 From 770b439c8495c3a0b16550c2f04740f31646d66b Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 10:36:29 +0100 Subject: WIP: add csrf token to every request --- service/pixelated/resources/__init__.py | 2 +- service/pixelated/resources/inbox_resource.py | 1 - service/pixelated/resources/root_resource.py | 6 +-- service/test/integration/test_delete_mail.py | 5 ++- service/test/integration/test_logout.py | 4 +- .../test/support/integration/app_test_client.py | 4 +- .../test/support/integration/multi_user_client.py | 8 +++- service/test/unit/resources/test_inbox_resource.py | 26 ------------ service/test/unit/resources/test_root_resource.py | 48 ++++++++++++++++------ 9 files changed, 55 insertions(+), 49 deletions(-) diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py index 97346a6f..023758de 100644 --- a/service/pixelated/resources/__init__.py +++ b/service/pixelated/resources/__init__.py @@ -66,7 +66,7 @@ class BaseResource(Resource): self._services_factory = services_factory def _add_csrf_cookie(self, request): - csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + csrf_token = IPixelatedSession(request.getSession()).get_csrf_token() request.addCookie('XSRF-TOKEN', csrf_token) log.debug('XSRF-TOKEN added: %s' % csrf_token) diff --git a/service/pixelated/resources/inbox_resource.py b/service/pixelated/resources/inbox_resource.py index 47a3c072..f759dca9 100644 --- a/service/pixelated/resources/inbox_resource.py +++ b/service/pixelated/resources/inbox_resource.py @@ -53,7 +53,6 @@ class InboxResource(BaseResource): def render_GET(self, request): logger.debug('Inbox rendering GET. %s' % self) - self._add_csrf_cookie(request) if self._is_starting(): logger.debug('Inbox rendering interstitial. %s' % self) return self.interstitial diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 7d5b0b0a..1d32935b 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -65,6 +65,7 @@ class RootResource(BaseResource): logger.debug('Root in STARTUP mode. %s' % self) def getChildWithDefault(self, path, request): + self._add_csrf_cookie(request) if path == '': return self._redirect_to_login_resource if self._public else self._inbox_resource if self._mode == MODE_STARTUP: @@ -81,7 +82,6 @@ class RootResource(BaseResource): xsrf_token = request.getCookie('XSRF-TOKEN') logger.debug('CSRF token: %s' % xsrf_token) - # TODO: how is comparing the cookie-csrf with the HTTP-header-csrf adding any csrf protection? ajax_request = (request.getHeader('x-requested-with') == 'XMLHttpRequest') if ajax_request: xsrf_header = request.getHeader('x-xsrf-token') @@ -101,7 +101,7 @@ class RootResource(BaseResource): return self.putChildProtected(path, resource) # to be on the safe side def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): - self.putChildProtected('sandbox', SandboxResource(self._static_folder)) + self.putChildPublic('sandbox', SandboxResource(self._static_folder)) self.putChildProtected('keys', KeysResource(self._services_factory)) self.putChildProtected(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) self.putChildProtected('contacts', ContactsResource(self._services_factory)) @@ -114,7 +114,7 @@ class RootResource(BaseResource): self.putChildProtected('users', UsersResource(self._services_factory)) self.putChildPublic(LoginResource.BASE_URL, LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) - self.putChildProtected(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) + self.putChildPublic(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) self._inbox_resource.initialize() self._mode = MODE_RUNNING diff --git a/service/test/integration/test_delete_mail.py b/service/test/integration/test_delete_mail.py index a912f9f0..6cb9ceb6 100644 --- a/service/test/integration/test_delete_mail.py +++ b/service/test/integration/test_delete_mail.py @@ -15,6 +15,7 @@ # along with Pixelated. If not, see . from twisted.internet import defer from test.support.integration import SoledadTestBase, MailBuilder +from pixelated.resources import IPixelatedSession class DeleteMailTest(SoledadTestBase): @@ -27,7 +28,9 @@ class DeleteMailTest(SoledadTestBase): inbox_mails = yield self.app_test_client.get_mails_by_tag('inbox') self.assertEquals(1, len(inbox_mails)) - yield self.app_test_client.delete_mail(mail.mail_id) + response, first_request = yield self.app_test_client.get('/', as_json=False) + csrftoken = IPixelatedSession(first_request.getSession()).get_csrf_token() + yield self.app_test_client.delete_mail(mail.mail_id, csrf=csrftoken) inbox_mails = yield self.app_test_client.get_mails_by_tag('inbox') self.assertEquals(0, len(inbox_mails)) diff --git a/service/test/integration/test_logout.py b/service/test/integration/test_logout.py index c9d39d17..b4f8ebf3 100644 --- a/service/test/integration/test_logout.py +++ b/service/test/integration/test_logout.py @@ -29,7 +29,8 @@ class MultiUserLogoutTest(MultiUserSoledadTestBase): @defer.inlineCallbacks def test_logout_deletes_services_stop_background_reactor_tasks_and_closes_soledad(self): - response, login_request = yield self.app_test_client.login() + response, first_request = yield self.app_test_client.get('/login', as_json=False) + response, login_request = yield self.app_test_client.login(from_request=first_request) yield response yield self.wait_for_session_user_id_to_finish() @@ -37,6 +38,7 @@ class MultiUserLogoutTest(MultiUserSoledadTestBase): response, request = self.app_test_client.post( "/logout", json.dumps({'csrftoken': [login_request.getCookie('XSRF-TOKEN')]}), + ajax=False, from_request=login_request, as_json=False) yield response diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index d52c85c0..ee5a1df2 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -387,8 +387,8 @@ class AppTestClient(object): return res # TODO: remove - def delete_mail(self, mail_ident): - res, req = self.delete("/mail/%s" % mail_ident) + def delete_mail(self, mail_ident, csrf='token'): + res, req = self.delete("/mail/%s" % mail_ident, csrf=csrf) return res def delete_mails(self, idents): diff --git a/service/test/support/integration/multi_user_client.py b/service/test/support/integration/multi_user_client.py index 82acb210..fe8595fb 100644 --- a/service/test/support/integration/multi_user_client.py +++ b/service/test/support/integration/multi_user_client.py @@ -24,6 +24,7 @@ from pixelated.config.services import ServicesFactory from pixelated.config.sessions import LeapSessionFactory import pixelated.config.services +from pixelated.resources import IPixelatedSession from pixelated.resources.root_resource import RootResource from test.support.integration import AppTestClient from test.support.integration.app_test_client import AppTestAccount, StubSRPChecker @@ -57,7 +58,7 @@ class MultiUserClient(AppTestClient): else: when(Authenticator)._bonafide_auth(username, password).thenRaise(SRPAuthError) - def login(self, username='username', password='password'): + def login(self, username='username', password='password', from_request=None): session = Authentication(username, 'some_user_token', 'some_user_uuid', 'session_id', {'is_admin': False}) leap_session = self._test_account.leap_session leap_session.user_auth = session @@ -76,7 +77,10 @@ class MultiUserClient(AppTestClient): when(leap_session).initial_sync().thenAnswer(lambda: defer.succeed(None)) when(pixelated.config.services).Services(ANY()).thenReturn(self.services) - request = request_mock(path='/login', method="POST", body={'username': username, 'password': password}) + session = from_request.getSession() + csrftoken = IPixelatedSession(session).get_csrf_token() + request = request_mock(path='/login', method="POST", body={'username': username, 'password': password, 'csrftoken': csrftoken}, ajax=False) + request.session = session return self._render(request, as_json=False) def get(self, path, get_args='', as_json=True, from_request=None): diff --git a/service/test/unit/resources/test_inbox_resource.py b/service/test/unit/resources/test_inbox_resource.py index 03fe6f1a..9af355ca 100644 --- a/service/test/unit/resources/test_inbox_resource.py +++ b/service/test/unit/resources/test_inbox_resource.py @@ -44,29 +44,3 @@ class TestInboxResource(unittest.TestCase): d.addCallback(assert_response) return d - - def _test_should_renew_xsrf_cookie(self): - request = DummyRequest(['']) - request.addCookie = MagicMock() - generated_csrf_token = 'csrf_token' - mock_sha = MagicMock() - mock_sha.hexdigest = MagicMock(return_value=generated_csrf_token) - - with patch('hashlib.sha256', return_value=mock_sha): - d = self.web.get(request) - - def assert_csrf_cookie(_): - request.addCookie.assert_called_once_with('XSRF-TOKEN', generated_csrf_token) - - d.addCallback(assert_csrf_cookie) - return d - - # TODO should this be here or just in the root resource test? - def test_should_renew_xsrf_cookie_on_startup_mode(self): - self.inbox_resource._mode = MODE_STARTUP - self._test_should_renew_xsrf_cookie() - - # TODO should this be here or just in the root resource test? - def test_should_renew_xsrf_cookie_on_running_mode(self): - self.inbox_resource._mode = MODE_RUNNING - self._test_should_renew_xsrf_cookie() diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index b674103c..2dfe3e5a 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -6,7 +6,7 @@ from mockito import mock, when, any as ANY import pixelated from pixelated.application import UserAgentMode -from pixelated.resources import UnAuthorizedResource +from pixelated.resources import IPixelatedSession, UnAuthorizedResource from pixelated.resources.features_resource import FeaturesResource from pixelated.resources.login_resource import LoginResource from test.unit.resources import DummySite @@ -30,7 +30,7 @@ class TestPublicRootResource(unittest.TestCase): url_fragment, resource_mock = 'some-url-fragment', mock() self.public_root_resource.putChildPublic(url_fragment, resource_mock) request = DummyRequest([url_fragment]) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.public_root_resource, request) self.assertIs(child_resource, resource_mock) @@ -39,7 +39,7 @@ class TestPublicRootResource(unittest.TestCase): url_fragment, resource_mock = 'some-url-fragment', mock() self.public_root_resource.putChildProtected(url_fragment, resource_mock) request = DummyRequest([url_fragment]) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, UnAuthorizedResource) @@ -48,14 +48,14 @@ class TestPublicRootResource(unittest.TestCase): url_fragment, resource_mock = 'some-url-fragment', mock() self.public_root_resource.putChild(url_fragment, resource_mock) request = DummyRequest([url_fragment]) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, UnAuthorizedResource) def test_private_resource_returns_401(self): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['mails']) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') d = self.web.get(request) def assert_unauthorized(request): @@ -68,14 +68,14 @@ class TestPublicRootResource(unittest.TestCase): def test_login_url_should_delegate_to_login_resource(self): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, LoginResource) def test_root_url_should_redirect_to_login_resource(self): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['']) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') d = self.web.get(request) def assert_redirect(request): @@ -107,7 +107,7 @@ class TestRootResource(unittest.TestCase): url_fragment, resource_mock = 'some-url-fragment', mock() self.root_resource.putChildProtected(url_fragment, resource_mock) request = DummyRequest([url_fragment]) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.root_resource, request) self.assertIs(child_resource, resource_mock) @@ -116,13 +116,13 @@ class TestRootResource(unittest.TestCase): url_fragment, resource_mock = 'some-url-fragment', mock() self.root_resource.putChild(url_fragment, resource_mock) request = DummyRequest([url_fragment]) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.root_resource, request) self.assertIs(child_resource, resource_mock) def test_root_url_should_delegate_to_inbox(self): request = DummyRequest(['']) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.root_resource, request) self.assertIsInstance(child_resource, InboxResource) @@ -130,13 +130,13 @@ class TestRootResource(unittest.TestCase): def test_login_url_should_delegate_to_login_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) - request.addCookie = lambda key, value: 'stubbed' + request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.root_resource, request) self.assertIsInstance(child_resource, LoginResource) def _test_should_renew_xsrf_cookie(self): request = DummyRequest(['']) - request.addCookie = MagicMock() + request.addCookie = MagicMock(return_value='stubbed') generated_csrf_token = 'csrf_token' mock_sha = MagicMock() mock_sha.hexdigest = MagicMock(return_value=generated_csrf_token) @@ -162,6 +162,7 @@ class TestRootResource(unittest.TestCase): self.root_resource._mode = MODE_STARTUP request = DummyRequest(['/child']) + request.addCookie = MagicMock(return_value='stubbed') request.getCookie = MagicMock(return_value='irrelevant -- stubbed') d = self.web.get(request) @@ -182,6 +183,7 @@ class TestRootResource(unittest.TestCase): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['/child']) + request.addCookie = MagicMock(return_value='stubbed') request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') @@ -198,6 +200,7 @@ class TestRootResource(unittest.TestCase): def test_GET_should_return_503_for_uninitialized_resource(self): request = DummyRequest(['/sandbox/']) + request.addCookie = MagicMock(return_value='stubbed') request.method = 'GET' request.getCookie = MagicMock(return_value='stubbed csrf token') @@ -215,6 +218,7 @@ class TestRootResource(unittest.TestCase): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['non-existing-child']) + request.addCookie = MagicMock(return_value='stubbed') request.method = 'GET' request.getCookie = MagicMock(return_value='stubbed csrf token') @@ -231,6 +235,7 @@ class TestRootResource(unittest.TestCase): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['non-existing-child']) + request.addCookie = MagicMock(return_value='stubbed') request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') request.getCookie = MagicMock(return_value='stubbed csrf token') @@ -246,6 +251,7 @@ class TestRootResource(unittest.TestCase): def test_should_authorize_child_resource_non_ajax_GET_requests(self): request = DummyRequest(['features']) + request.addCookie = MagicMock(return_value='stubbed') request.getCookie = MagicMock(return_value='irrelevant -- stubbed') self.root_resource.putChild('features', FeaturesResource()) @@ -270,6 +276,7 @@ class TestRootResource(unittest.TestCase): mock_content.read = MagicMock(return_value={}) request.content = mock_content + request.addCookie = MagicMock(return_value='stubbed') request.getCookie = MagicMock(return_value='mismatched csrf token') d = self.web.get(request) @@ -286,6 +293,7 @@ class TestRootResource(unittest.TestCase): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['assets', 'dummy.json']) + request.addCookie = MagicMock(return_value='stubbed') d = self.web.get(request) def assert_response(_): @@ -299,6 +307,7 @@ class TestRootResource(unittest.TestCase): self.root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) + request.addCookie = MagicMock(return_value='stubbed') d = self.web.get(request) def assert_response(_): @@ -309,6 +318,7 @@ class TestRootResource(unittest.TestCase): def test_root_should_be_handled_by_inbox_resource(self): request = DummyRequest([]) + request.addCookie = MagicMock(return_value='stubbed') request.prepath = [''] request.path = '/' # TODO: setup mocked portal @@ -318,9 +328,23 @@ class TestRootResource(unittest.TestCase): def test_inbox_should_not_be_public(self): request = DummyRequest([]) + request.addCookie = MagicMock(return_value='stubbed') request.prepath = [''] request.path = '/' # TODO: setup mocked portal resource = self.root_resource.getChildWithDefault(request.prepath[-1], request) self.assertIsInstance(resource, InboxResource) + + def test_every_url_should_get_csrftoken_header(self): + # self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['any']) + request.addCookie = MagicMock(return_value='stubbed') + d = self.web.get(request) + + def assert_add_cookie_called_for_csrftoken(request): + csrftoken = IPixelatedSession(request.getSession()).get_csrf_token() + self.assertEqual([(('XSRF-TOKEN', csrftoken),)], request.addCookie.call_args_list) + + d.addCallback(assert_add_cookie_called_for_csrftoken) + return d -- cgit v1.2.3 From 875249af34fc5a53b727fe8b8296a5d4206c11c7 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 13:39:37 +0100 Subject: fix root resource tests when zmq is not available --- service/test/unit/resources/test_root_resource.py | 24 +++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 2dfe3e5a..9d738a83 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -25,7 +25,8 @@ class TestPublicRootResource(unittest.TestCase): self.public_root_resource = RootResource(mock(), public=True) self.web = DummySite(self.public_root_resource) - def test_put_child_public_adds_resource(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_put_child_public_adds_resource(self, *mocks): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) url_fragment, resource_mock = 'some-url-fragment', mock() self.public_root_resource.putChildPublic(url_fragment, resource_mock) @@ -34,7 +35,8 @@ class TestPublicRootResource(unittest.TestCase): child_resource = getChildForRequest(self.public_root_resource, request) self.assertIs(child_resource, resource_mock) - def test_put_child_protected_adds_unauthorized(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_put_child_protected_adds_unauthorized(self, *mocks): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) url_fragment, resource_mock = 'some-url-fragment', mock() self.public_root_resource.putChildProtected(url_fragment, resource_mock) @@ -43,7 +45,8 @@ class TestPublicRootResource(unittest.TestCase): child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, UnAuthorizedResource) - def test_put_child_adds_unauthorized(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_put_child_adds_unauthorized(self, *mocks): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) url_fragment, resource_mock = 'some-url-fragment', mock() self.public_root_resource.putChild(url_fragment, resource_mock) @@ -52,7 +55,8 @@ class TestPublicRootResource(unittest.TestCase): child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, UnAuthorizedResource) - def test_private_resource_returns_401(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_private_resource_returns_401(self, *mocks): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['mails']) request.addCookie = MagicMock(return_value='stubbed') @@ -65,14 +69,16 @@ class TestPublicRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d - def test_login_url_should_delegate_to_login_resource(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_login_url_should_delegate_to_login_resource(self, *mocks): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['login']) request.addCookie = MagicMock(return_value='stubbed') child_resource = getChildForRequest(self.public_root_resource, request) self.assertIsInstance(child_resource, LoginResource) - def test_root_url_should_redirect_to_login_resource(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_root_url_should_redirect_to_login_resource(self, *mocks): self.public_root_resource.initialize(provider=mock(), authenticator=mock()) request = DummyRequest(['']) request.addCookie = MagicMock(return_value='stubbed') @@ -102,7 +108,8 @@ class TestRootResource(unittest.TestCase): self.root_resource = RootResource(self.services_factory) self.web = DummySite(self.root_resource) - def test_put_child_protected_adds_resource(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_put_child_protected_adds_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) url_fragment, resource_mock = 'some-url-fragment', mock() self.root_resource.putChildProtected(url_fragment, resource_mock) @@ -111,7 +118,8 @@ class TestRootResource(unittest.TestCase): child_resource = getChildForRequest(self.root_resource, request) self.assertIs(child_resource, resource_mock) - def test_put_child_adds_resource(self): + @patch('pixelated.resources.mails_resource.events.register') + def test_put_child_adds_resource(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) url_fragment, resource_mock = 'some-url-fragment', mock() self.root_resource.putChild(url_fragment, resource_mock) -- cgit v1.2.3 From f0880aff32bbb30c6a8a0d4e078e563d24b97909 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 15:56:57 +0100 Subject: fix csrf for some integration tests --- service/test/integration/test_delete_mail.py | 12 +++++++----- service/test/integration/test_logout.py | 4 ++-- service/test/integration/test_multi_user_login.py | 8 +++++--- service/test/integration/test_users_count.py | 5 +++-- service/test/support/integration/app_test_client.py | 19 +++++++++++++------ .../test/support/integration/multi_user_client.py | 21 +++++++++------------ 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/service/test/integration/test_delete_mail.py b/service/test/integration/test_delete_mail.py index 6cb9ceb6..34ea5048 100644 --- a/service/test/integration/test_delete_mail.py +++ b/service/test/integration/test_delete_mail.py @@ -29,8 +29,7 @@ class DeleteMailTest(SoledadTestBase): self.assertEquals(1, len(inbox_mails)) response, first_request = yield self.app_test_client.get('/', as_json=False) - csrftoken = IPixelatedSession(first_request.getSession()).get_csrf_token() - yield self.app_test_client.delete_mail(mail.mail_id, csrf=csrftoken) + yield self.app_test_client.delete_mail(mail.mail_id, session=first_request.getSession()) inbox_mails = yield self.app_test_client.get_mails_by_tag('inbox') self.assertEquals(0, len(inbox_mails)) @@ -40,7 +39,8 @@ class DeleteMailTest(SoledadTestBase): @defer.inlineCallbacks def test_delete_mail_when_trashing_mail_from_trash_mailbox(self): mails = yield self.app_test_client.add_multiple_to_mailbox(1, 'trash') - yield self.app_test_client.delete_mails([mails[0].ident]) + response, first_request = yield self.app_test_client.get('/', as_json=False) + yield self.app_test_client.delete_mails([mails[0].ident], session=first_request.getSession()) trash_mails = yield self.app_test_client.get_mails_by_tag('trash') @@ -52,7 +52,8 @@ class DeleteMailTest(SoledadTestBase): mails = yield self.app_test_client.add_multiple_to_mailbox(5, 'inbox') mail_idents = [m.ident for m in mails] - yield self.app_test_client.delete_mails(mail_idents) + response, first_request = yield self.app_test_client.get('/', as_json=False) + yield self.app_test_client.delete_mails(mail_idents, session=first_request.getSession()) inbox = yield self.app_test_client.get_mails_by_tag('inbox') self.assertEquals(0, len(inbox)) @@ -62,7 +63,8 @@ class DeleteMailTest(SoledadTestBase): mails = yield self.app_test_client.add_multiple_to_mailbox(5, 'trash') mail_idents = [m.ident for m in mails] - yield self.app_test_client.delete_mails(mail_idents) + response, first_request = yield self.app_test_client.get('/', as_json=False) + yield self.app_test_client.delete_mails(mail_idents, session=first_request.getSession()) trash = yield self.app_test_client.get_mails_by_tag('trash') self.assertEquals(0, len(trash)) diff --git a/service/test/integration/test_logout.py b/service/test/integration/test_logout.py index b4f8ebf3..92c2afe5 100644 --- a/service/test/integration/test_logout.py +++ b/service/test/integration/test_logout.py @@ -30,7 +30,7 @@ class MultiUserLogoutTest(MultiUserSoledadTestBase): @defer.inlineCallbacks def test_logout_deletes_services_stop_background_reactor_tasks_and_closes_soledad(self): response, first_request = yield self.app_test_client.get('/login', as_json=False) - response, login_request = yield self.app_test_client.login(from_request=first_request) + response, login_request = yield self.app_test_client.login(session=first_request.getSession()) yield response yield self.wait_for_session_user_id_to_finish() @@ -39,7 +39,7 @@ class MultiUserLogoutTest(MultiUserSoledadTestBase): "/logout", json.dumps({'csrftoken': [login_request.getCookie('XSRF-TOKEN')]}), ajax=False, - from_request=login_request, + session=login_request.getSession(), as_json=False) yield response diff --git a/service/test/integration/test_multi_user_login.py b/service/test/integration/test_multi_user_login.py index af2a81ac..e1f58202 100644 --- a/service/test/integration/test_multi_user_login.py +++ b/service/test/integration/test_multi_user_login.py @@ -33,13 +33,14 @@ class MultiUserLoginTest(MultiUserSoledadTestBase): @defer.inlineCallbacks def test_logged_in_users_sees_resources(self): - response, login_request = yield self.app_test_client.login() + response, first_request = yield self.app_test_client.get('/login', as_json=False) + response, login_request = yield self.app_test_client.login(session=first_request.getSession()) yield response mail = load_mail_from_file('mbox00000000') mail_id = yield self._create_mail_in_soledad(mail) expected_mail_dict = {'body': u'Dignissimos ducimus veritatis. Est tenetur consequatur quia occaecati. Vel sit sit voluptas.\n\nEarum distinctio eos. Accusantium qui sint ut quia assumenda. Facere dignissimos inventore autem sit amet. Pariatur voluptatem sint est.\n\nUt recusandae praesentium aspernatur. Exercitationem amet placeat deserunt quae consequatur eum. Unde doloremque suscipit quia.\n\n', 'header': {u'date': u'Tue, 21 Apr 2015 08:43:27 +0000 (UTC)', u'to': [u'carmel@murazikortiz.name'], u'x-tw-pixelated-tags': u'nite, macro, trash', u'from': u'darby.senger@zemlak.biz', u'subject': u'Itaque consequatur repellendus provident sunt quia.'}, 'ident': mail_id, 'status': [], 'tags': [], 'textPlainBody': u'Dignissimos ducimus veritatis. Est tenetur consequatur quia occaecati. Vel sit sit voluptas.\n\nEarum distinctio eos. Accusantium qui sint ut quia assumenda. Facere dignissimos inventore autem sit amet. Pariatur voluptatem sint est.\n\nUt recusandae praesentium aspernatur. Exercitationem amet placeat deserunt quae consequatur eum. Unde doloremque suscipit quia.\n\n', 'mailbox': u'inbox', 'attachments': [], 'security_casing': {'imprints': [{'state': 'no_signature_information'}], 'locks': []}} - response, request = self.app_test_client.get("/mail/%s" % mail_id, from_request=login_request) + response, request = self.app_test_client.get("/mail/%s" % mail_id, session=login_request.getSession()) response = yield response self.assertEqual(200, request.code) @@ -48,7 +49,8 @@ class MultiUserLoginTest(MultiUserSoledadTestBase): @defer.inlineCallbacks def test_wrong_credentials_cannot_access_resources(self): - response, login_request = self.app_test_client.login('username', 'wrong_password') + response, first_request = yield self.app_test_client.get('/login', as_json=False) + response, login_request = self.app_test_client.login('username', 'wrong_password', session=first_request.getSession()) response_str = yield response self.assertEqual(401, login_request.responseCode) self.assertIn('Invalid credentials', login_request.written) diff --git a/service/test/integration/test_users_count.py b/service/test/integration/test_users_count.py index a03adacf..a9813b2c 100644 --- a/service/test/integration/test_users_count.py +++ b/service/test/integration/test_users_count.py @@ -31,7 +31,8 @@ class UsersResourceTest(MultiUserSoledadTestBase): @defer.inlineCallbacks def test_online_users_count_uses_leap_auth_privileges(self): - response, login_request = yield self.app_test_client.login() + response, first_request = yield self.app_test_client.get('/', as_json=False) + response, login_request = yield self.app_test_client.login(session=first_request.getSession()) yield response yield self.wait_for_session_user_id_to_finish() @@ -40,7 +41,7 @@ class UsersResourceTest(MultiUserSoledadTestBase): response, request = self.app_test_client.get( "/users", json.dumps({'csrftoken': [login_request.getCookie('XSRF-TOKEN')]}), - from_request=login_request, + session=login_request.getSession(), as_json=False) yield response diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index ee5a1df2..9ab74261 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -49,6 +49,7 @@ from pixelated.adapter.search import SearchEngine from pixelated.adapter.services.draft_service import DraftService from pixelated.adapter.services.mail_service import MailService from pixelated.resources.root_resource import RootResource +from pixelated.resources.session import IPixelatedSession from test.support.integration.model import MailBuilder from test.support.test_helper import request_mock from test.support.integration.model import ResponseMail @@ -278,17 +279,21 @@ class AppTestClient(object): request.args = get_args return self._render(request, as_json) - def post(self, path, body='', headers=None, ajax=True, csrf='token'): + def post(self, path, body='', headers=None, ajax=True, csrf='token', session=None): headers = headers or {'Content-Type': 'application/json'} request = request_mock(path=path, method="POST", body=body, headers=headers, ajax=ajax, csrf=csrf) + if session: + request.session = session return self._render(request) def put(self, path, body, ajax=True, csrf='token'): request = request_mock(path=path, method="PUT", body=body, headers={'Content-Type': ['application/json']}, ajax=ajax, csrf=csrf) return self._render(request) - def delete(self, path, body="", ajax=True, csrf='token'): + def delete(self, path, body="", ajax=True, csrf='token', session=None): request = request_mock(path=path, body=body, headers={'Content-Type': ['application/json']}, method="DELETE", ajax=ajax, csrf=csrf) + if session: + request.session = session return self._render(request) @defer.inlineCallbacks @@ -387,12 +392,14 @@ class AppTestClient(object): return res # TODO: remove - def delete_mail(self, mail_ident, csrf='token'): - res, req = self.delete("/mail/%s" % mail_ident, csrf=csrf) + def delete_mail(self, mail_ident, session): + csrf = IPixelatedSession(session).get_csrf_token() + res, req = self.delete("/mail/%s" % mail_ident, csrf=csrf, session=session) return res - def delete_mails(self, idents): - res, req = self.post("/mails/delete", json.dumps({'idents': idents})) + def delete_mails(self, idents, session): + csrf = IPixelatedSession(session).get_csrf_token() + res, req = self.post("/mails/delete", json.dumps({'idents': idents}), csrf=csrf, session=session) return res def mark_many_as_unread(self, idents): diff --git a/service/test/support/integration/multi_user_client.py b/service/test/support/integration/multi_user_client.py index fe8595fb..4b9b2864 100644 --- a/service/test/support/integration/multi_user_client.py +++ b/service/test/support/integration/multi_user_client.py @@ -58,44 +58,41 @@ class MultiUserClient(AppTestClient): else: when(Authenticator)._bonafide_auth(username, password).thenRaise(SRPAuthError) - def login(self, username='username', password='password', from_request=None): - session = Authentication(username, 'some_user_token', 'some_user_uuid', 'session_id', {'is_admin': False}) + def login(self, username='username', password='password', session=None): + auth_session = Authentication(username, 'some_user_token', 'some_user_uuid', 'session_id', {'is_admin': False}) leap_session = self._test_account.leap_session - leap_session.user_auth = session + leap_session.user_auth = auth_session config = mock() config.leap_home = 'some_folder' leap_session.config = config leap_session.fresh_account = False self.leap_session = leap_session self.services = self._test_account.services - self.user_auth = session + self.user_auth = auth_session self._mock_bonafide_auth(username, password) - when(LeapSessionFactory).create(username, password, session).thenReturn(leap_session) + when(LeapSessionFactory).create(username, password, auth_session).thenReturn(leap_session) with patch('mockito.invocation.AnswerSelector', AnswerSelector): when(leap_session).initial_sync().thenAnswer(lambda: defer.succeed(None)) when(pixelated.config.services).Services(ANY()).thenReturn(self.services) - session = from_request.getSession() csrftoken = IPixelatedSession(session).get_csrf_token() request = request_mock(path='/login', method="POST", body={'username': username, 'password': password, 'csrftoken': csrftoken}, ajax=False) request.session = session return self._render(request, as_json=False) - def get(self, path, get_args='', as_json=True, from_request=None): + def get(self, path, get_args='', as_json=True, session=None): request = request_mock(path) request.args = get_args - if from_request: - session = from_request.getSession() + if session: request.session = session return self._render(request, as_json) - def post(self, path, body='', headers=None, ajax=True, csrf='token', as_json=True, from_request=None): + def post(self, path, body='', headers=None, ajax=True, csrf='token', as_json=True, session=None): headers = headers or {'Content-Type': 'application/json'} request = request_mock(path=path, method="POST", body=body, headers=headers, ajax=ajax, csrf=csrf) - if from_request: - session = from_request.getSession() + if session: request.session = session return self._render(request, as_json) -- cgit v1.2.3 From 165ab49e41faa7ba7d524c58b3b0d383a4c9a2d9 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 17:35:33 +0100 Subject: use the right inbox template --- service/pixelated/resources/inbox_resource.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/service/pixelated/resources/inbox_resource.py b/service/pixelated/resources/inbox_resource.py index f759dca9..426de5cc 100644 --- a/service/pixelated/resources/inbox_resource.py +++ b/service/pixelated/resources/inbox_resource.py @@ -34,15 +34,19 @@ class InboxResource(BaseResource): def __init__(self, services_factory): BaseResource.__init__(self, services_factory) + self._not_quite_the_templates_folder = self._get_not_quite_the_templates_folder() self._templates_folder = self._get_templates_folder() - self._html_template = open(os.path.join(self._templates_folder, 'index.html')).read() + self._html_template = open(os.path.join(self._not_quite_the_templates_folder, 'index.html')).read() with open(os.path.join(self._templates_folder, 'Interstitial.html')) as f: self.interstitial = f.read() self._mode = MODE_STARTUP def initialize(self): self._mode = MODE_RUNNING - logger.debug('Inbox in RUNNING mode. %s' % self) + + def _get_not_quite_the_templates_folder(self): + path = os.path.dirname(os.path.abspath(pixelated.__file__)) + return os.path.join(path, '..', '..', 'web-ui', 'app') def _get_templates_folder(self): path = os.path.dirname(os.path.abspath(pixelated.__file__)) @@ -52,12 +56,9 @@ class InboxResource(BaseResource): return self._mode == MODE_STARTUP def render_GET(self, request): - logger.debug('Inbox rendering GET. %s' % self) if self._is_starting(): - logger.debug('Inbox rendering interstitial. %s' % self) return self.interstitial else: - logger.debug('Inbox rendering from template. %s' % self) account_email = self.mail_service(request).account_email response = Template(self._html_template).safe_substitute(account_email=account_email) return str(response) -- cgit v1.2.3 From 20b1922794d3179b32dd930706ec5693a3562464 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 18:08:12 +0100 Subject: fix csrf in drafts tests --- service/test/integration/test_drafts.py | 30 ++++++++++++++++------ .../test/support/integration/app_test_client.py | 9 ++++--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/service/test/integration/test_drafts.py b/service/test/integration/test_drafts.py index 657cfab1..a9c7b3f7 100644 --- a/service/test/integration/test_drafts.py +++ b/service/test/integration/test_drafts.py @@ -17,6 +17,7 @@ from test.support.integration import SoledadTestBase, MailBuilder from mockito import unstub, when, any from twisted.internet import defer +from pixelated.resources import IPixelatedSession class DraftsTest(SoledadTestBase): @@ -26,17 +27,20 @@ class DraftsTest(SoledadTestBase): @defer.inlineCallbacks def test_post_sends_mail_and_deletes_previous_draft_if_it_exists(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + # act as if sending the mail by SMTP succeeded sendmail_deferred = defer.Deferred() when(self.app_test_client.mail_sender).sendmail(any()).thenReturn(sendmail_deferred) # creates one draft first_draft = MailBuilder().with_subject('First draft').build_json() - first_draft_ident = (yield self.app_test_client.put_mail(first_draft)[0])['ident'] + first_draft_ident = (yield self.app_test_client.put_mail(first_draft, session=session)[0])['ident'] # sends an updated version of the draft second_draft = MailBuilder().with_subject('Second draft').with_ident(first_draft_ident).build_json() - deferred_res = self.post_mail(second_draft) + deferred_res = self.post_mail(second_draft, session) sendmail_deferred.callback(None) # SMTP succeeded @@ -54,12 +58,15 @@ class DraftsTest(SoledadTestBase): @defer.inlineCallbacks def test_post_sends_mail_even_when_draft_does_not_exist(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + # act as if sending the mail by SMTP succeeded sendmail_deferred = defer.Deferred() when(self.app_test_client.mail_sender).sendmail(any()).thenReturn(sendmail_deferred) first_draft = MailBuilder().with_subject('First draft').build_json() - res = self.post_mail(first_draft) + res = self.post_mail(first_draft, session) sendmail_deferred.callback(True) yield res @@ -70,25 +77,32 @@ class DraftsTest(SoledadTestBase): self.assertEquals('First draft', sent_mails[0].subject) self.assertEquals(0, len(drafts)) - def post_mail(self, data): - deferred_res, req = self.app_test_client.post('/mails', data) + def post_mail(self, data, session): + csrf = IPixelatedSession(session).get_csrf_token() + deferred_res, req = self.app_test_client.post('/mails', data, csrf=csrf, session=session) return deferred_res @defer.inlineCallbacks def test_put_creates_a_draft_if_it_does_not_exist(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + mail = MailBuilder().with_subject('A new draft').build_json() - yield self.app_test_client.put_mail(mail)[0] + yield self.app_test_client.put_mail(mail, session=session)[0] mails = yield self.app_test_client.get_mails_by_tag('drafts') self.assertEquals('A new draft', mails[0].subject) @defer.inlineCallbacks def test_put_updates_draft_if_it_already_exists(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + draft = MailBuilder().with_subject('First draft').build_json() - draft_ident = (yield self.app_test_client.put_mail(draft)[0])['ident'] + draft_ident = (yield self.app_test_client.put_mail(draft, session=session)[0])['ident'] updated_draft = MailBuilder().with_subject('First draft edited').with_ident(draft_ident).build_json() - yield self.app_test_client.put_mail(updated_draft)[0] + yield self.app_test_client.put_mail(updated_draft, session=session)[0] drafts = yield self.app_test_client.get_mails_by_tag('drafts') diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index 9ab74261..f04f67fd 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -286,8 +286,10 @@ class AppTestClient(object): request.session = session return self._render(request) - def put(self, path, body, ajax=True, csrf='token'): + def put(self, path, body, ajax=True, csrf='token', session=None): request = request_mock(path=path, method="PUT", body=body, headers={'Content-Type': ['application/json']}, ajax=ajax, csrf=csrf) + if session: + request.session = session return self._render(request) def delete(self, path, body="", ajax=True, csrf='token', session=None): @@ -375,8 +377,9 @@ class AppTestClient(object): res = yield deferred_result defer.returnValue((res, req)) - def put_mail(self, data): - res, req = self.put('/mails', data) + def put_mail(self, data, session): + csrf = IPixelatedSession(session).get_csrf_token() + res, req = self.put('/mails', data, csrf=csrf, session=session) return res, req def post_tags(self, mail_ident, tags_json): -- cgit v1.2.3 From 05551265c641ac51d897a49e35f390fde7bc4d8c Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 18:20:38 +0100 Subject: fix csrf in mark as read/unread tests --- .../test/integration/test_mark_as_read_unread.py | 29 ++++++++++++++++------ .../test/support/integration/app_test_client.py | 10 +++++--- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/service/test/integration/test_mark_as_read_unread.py b/service/test/integration/test_mark_as_read_unread.py index 18c3ddc2..c01deefc 100644 --- a/service/test/integration/test_mark_as_read_unread.py +++ b/service/test/integration/test_mark_as_read_unread.py @@ -30,32 +30,40 @@ class MarkAsReadUnreadTest(SoledadTestBase): mails = yield self.app_test_client.get_mails_by_tag('inbox') self.assertNotIn('read', mails[0].status) - yield self.app_test_client.mark_many_as_read([mail.ident]) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + yield self.app_test_client.mark_many_as_read([mail.ident], session) mails = yield self.app_test_client.get_mails_by_tag('inbox') self.assertIn('read', mails[0].status) @defer.inlineCallbacks def test_mark_single_as_unread(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + input_mail = MailBuilder().build_input_mail() mail = yield self.app_test_client.add_mail_to_inbox(input_mail) - yield self.app_test_client.mark_many_as_read([mail.ident]) + yield self.app_test_client.mark_many_as_read([mail.ident], session) - yield self.app_test_client.mark_many_as_unread([mail.ident]) + yield self.app_test_client.mark_many_as_unread([mail.ident], session) result = (yield self.app_test_client.get_mails_by_tag('inbox'))[0] self.assertNotIn('read', result.status) @defer.inlineCallbacks def test_mark_many_mails_as_unread(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + input_mail = MailBuilder().with_status([Status.SEEN]).build_input_mail() input_mail2 = MailBuilder().with_status([Status.SEEN]).build_input_mail() mail1 = yield self.app_test_client.add_mail_to_inbox(input_mail) mail2 = yield self.app_test_client.add_mail_to_inbox(input_mail2) - yield self.app_test_client.mark_many_as_read([mail1.ident, mail2.ident]) + yield self.app_test_client.mark_many_as_read([mail1.ident, mail2.ident], session) - yield self.app_test_client.mark_many_as_unread([mail1.ident, mail2.ident]) + yield self.app_test_client.mark_many_as_unread([mail1.ident, mail2.ident], session) mails = yield self.app_test_client.get_mails_by_tag('inbox') @@ -75,7 +83,9 @@ class MarkAsReadUnreadTest(SoledadTestBase): self.assertNotIn('read', mails[0].status) self.assertNotIn('read', mails[1].status) - yield self.app_test_client.mark_many_as_read([mails[0].ident, mails[1].ident]) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + yield self.app_test_client.mark_many_as_read([mails[0].ident, mails[1].ident], session) mails = yield self.app_test_client.get_mails_by_tag('inbox') @@ -84,12 +94,15 @@ class MarkAsReadUnreadTest(SoledadTestBase): @defer.inlineCallbacks def test_mark_mixed_status_as_read(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + input_mail = MailBuilder().with_subject('first').build_input_mail() input_mail2 = MailBuilder().with_subject('second').build_input_mail() yield self.app_test_client.add_mail_to_inbox(input_mail) mail2 = yield self.app_test_client.add_mail_to_inbox(input_mail2) - yield self.app_test_client.mark_many_as_read([mail2.ident]) + yield self.app_test_client.mark_many_as_read([mail2.ident], session) mails = yield self.app_test_client.get_mails_by_tag('inbox') @@ -98,7 +111,7 @@ class MarkAsReadUnreadTest(SoledadTestBase): self.assertEquals(1, len(unread_mails)) self.assertEquals(1, len(read_mails)) - yield self.app_test_client.mark_many_as_read([mails[0].ident, mails[1].ident]) + yield self.app_test_client.mark_many_as_read([mails[0].ident, mails[1].ident], session) mails = yield self.app_test_client.get_mails_by_tag('inbox') diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index f04f67fd..e5d42505 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -405,12 +405,14 @@ class AppTestClient(object): res, req = self.post("/mails/delete", json.dumps({'idents': idents}), csrf=csrf, session=session) return res - def mark_many_as_unread(self, idents): - res, req = self.post('/mails/unread', json.dumps({'idents': idents})) + def mark_many_as_unread(self, idents, session): + csrf = IPixelatedSession(session).get_csrf_token() + res, req = self.post('/mails/unread', json.dumps({'idents': idents}), csrf=csrf, session=session) return res - def mark_many_as_read(self, idents): - res, req = self.post('/mails/read', json.dumps({'idents': idents})) + def mark_many_as_read(self, idents, session): + csrf = IPixelatedSession(session).get_csrf_token() + res, req = self.post('/mails/read', json.dumps({'idents': idents}), csrf=csrf, session=session) return res def get_contacts(self, query): -- cgit v1.2.3 From 082d6a133a892226e6436aab26dd61f759cad30e Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 18:25:11 +0100 Subject: fix csrf in retrieve attachment test --- service/test/integration/test_retrieve_attachment.py | 4 +++- service/test/support/integration/app_test_client.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/service/test/integration/test_retrieve_attachment.py b/service/test/integration/test_retrieve_attachment.py index b46d40d5..ac6e52e7 100644 --- a/service/test/integration/test_retrieve_attachment.py +++ b/service/test/integration/test_retrieve_attachment.py @@ -86,7 +86,9 @@ class RetrieveAttachmentTest(SoledadTestBase): datagen, headers = multipart_encode([file]) post_data = "".join(datagen) - _, req = yield self.app_test_client.post_attachment(post_data, headers) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + _, req = yield self.app_test_client.post_attachment(post_data, headers, session) self.assertEqual(201, req.code) self.assertEqual('/attachment/B5B4ED80AC3B894523D72E375DACAA2FC6606C18EDF680FE95903086C8B5E14A', req.responseHeaders.getRawHeaders('location')[0]) diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index e5d42505..0bc2eacb 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -372,8 +372,9 @@ class AppTestClient(object): defer.returnValue((res, req)) @defer.inlineCallbacks - def post_attachment(self, data, headers): - deferred_result, req = self.post('/attachment', body=data, headers=headers) + def post_attachment(self, data, headers, session): + csrf = IPixelatedSession(session).get_csrf_token() + deferred_result, req = self.post('/attachment', body=data, headers=headers, csrf=csrf, session=session) res = yield deferred_result defer.returnValue((res, req)) -- cgit v1.2.3 From 688a8b42e8ab7c6d4529b6dda66f40eead07ad02 Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Thu, 1 Dec 2016 18:30:25 +0100 Subject: fix csrf in tags tests --- service/test/integration/test_tags.py | 23 ++++++++++++++++------ .../test/support/integration/app_test_client.py | 5 +++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/service/test/integration/test_tags.py b/service/test/integration/test_tags.py index 555a7382..d107e320 100644 --- a/service/test/integration/test_tags.py +++ b/service/test/integration/test_tags.py @@ -31,7 +31,9 @@ class TagsTest(SoledadTestBase): input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() mail = yield self.app_test_client.add_mail_to_inbox(input_mail) - yield self.app_test_client.post_tags(mail.ident, self._tags_json(['IMPORTANT'])) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + yield self.app_test_client.post_tags(mail.ident, self._tags_json(['IMPORTANT']), session) mails = yield self.app_test_client.get_mails_by_tag('inbox') self.assertEquals({'IMPORTANT'}, set(mails[0].tags)) @@ -41,15 +43,18 @@ class TagsTest(SoledadTestBase): @defer.inlineCallbacks def test_use_old_casing_when_same_tag_with_different_casing_is_posted(self): + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() mail = yield self.app_test_client.add_mail_to_inbox(input_mail) - yield self.app_test_client.post_tags(mail.ident, self._tags_json(['ImPoRtAnT'])) + yield self.app_test_client.post_tags(mail.ident, self._tags_json(['ImPoRtAnT']), session) mails = yield self.app_test_client.get_mails_by_tag('ImPoRtAnT') self.assertEquals({'ImPoRtAnT'}, set(mails[0].tags)) another_input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() another_mail = yield self.app_test_client.add_mail_to_inbox(another_input_mail) - yield self.app_test_client.post_tags(another_mail.ident, self._tags_json(['IMPORTANT'])) + yield self.app_test_client.post_tags(another_mail.ident, self._tags_json(['IMPORTANT']), session) mails = yield self.app_test_client.get_mails_by_tag('IMPORTANT') self.assertEquals(0, len(mails)) mails = yield self.app_test_client.get_mails_by_tag('ImPoRtAnT') @@ -62,7 +67,9 @@ class TagsTest(SoledadTestBase): input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() mail = yield self.app_test_client.add_mail_to_inbox(input_mail) - yield self.app_test_client.post_tags(mail.ident, self._tags_json(['ImPoRtAnT'])) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + yield self.app_test_client.post_tags(mail.ident, self._tags_json(['ImPoRtAnT']), session) mails = yield self.app_test_client.get_mails_by_tag('important') self.assertEquals(0, len(mails)) @@ -78,7 +85,9 @@ class TagsTest(SoledadTestBase): input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() mail = yield self.app_test_client.add_mail_to_inbox(input_mail) - yield self.app_test_client.post_tags(mail.ident, self._tags_json(['tag1', ' '])) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() + yield self.app_test_client.post_tags(mail.ident, self._tags_json(['tag1', ' ']), session) mail = yield self.app_test_client.get_mail(mail.ident) @@ -89,8 +98,10 @@ class TagsTest(SoledadTestBase): input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() mail = yield self.app_test_client.add_mail_to_inbox(input_mail) + response, first_request = yield self.app_test_client.get('/', as_json=False) + session = first_request.getSession() for tag in SPECIAL_TAGS: - response = yield self.app_test_client.post_tags(mail.ident, self._tags_json([tag.name.upper()])) + response = yield self.app_test_client.post_tags(mail.ident, self._tags_json([tag.name.upper()]), session) self.assertEquals("None of the following words can be used as tags: %s" % tag.name, response) mail = yield self.app_test_client.mail_store.get_mail(mail.ident) diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index 0bc2eacb..4e3758c5 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -383,8 +383,9 @@ class AppTestClient(object): res, req = self.put('/mails', data, csrf=csrf, session=session) return res, req - def post_tags(self, mail_ident, tags_json): - res, req = self.post("/mail/%s/tags" % mail_ident, tags_json) + def post_tags(self, mail_ident, tags_json, session): + csrf = IPixelatedSession(session).get_csrf_token() + res, req = self.post("/mail/%s/tags" % mail_ident, tags_json, csrf=csrf, session=session) return res def get_tags(self, **kwargs): -- cgit v1.2.3 From b14833fbb56bcd5bff0750c16fd9214009b955be Mon Sep 17 00:00:00 2001 From: Zara Gebru Date: Fri, 2 Dec 2016 15:25:23 +0100 Subject: [refactor] move app dir into public dir --- .gitignore | 6 +- doc/first-steps.md | 4 +- service/pixelated/application.py | 14 +- service/pixelated/resources/inbox_resource.py | 2 +- service/pixelated/resources/login_resource.py | 4 +- service/pixelated/resources/root_resource.py | 17 +- service/test/integration/test_static_files.py | 27 + .../test/support/integration/app_test_client.py | 5 +- .../test/support/integration/multi_user_client.py | 4 +- service/test/unit/resources/test_auth.py | 6 +- service/test/unit/resources/test_root_resource.py | 6 +- web-ui/.bowerrc | 2 +- web-ui/.jshintignore | 8 +- web-ui/.tx/config | 4 +- web-ui/app/404.html | 157 -- web-ui/app/favicon.ico | 0 web-ui/app/fonts/OpenSans-Bold.woff | Bin 14504 -> 0 bytes web-ui/app/fonts/OpenSans-BoldItalic.woff | Bin 15488 -> 0 bytes web-ui/app/fonts/OpenSans-Extrabold.woff | Bin 15312 -> 0 bytes web-ui/app/fonts/OpenSans-ExtraboldItalic.woff | Bin 15932 -> 0 bytes web-ui/app/fonts/OpenSans-Italic.woff | Bin 15768 -> 0 bytes web-ui/app/fonts/OpenSans-Light.woff | Bin 15048 -> 0 bytes web-ui/app/fonts/OpenSans-Semibold.woff | Bin 15236 -> 0 bytes web-ui/app/fonts/OpenSans-SemiboldItalic.woff | Bin 15736 -> 0 bytes web-ui/app/fonts/OpenSans.woff | Bin 14604 -> 0 bytes web-ui/app/fonts/OpenSansLight-Italic.woff | Bin 15956 -> 0 bytes web-ui/app/fonts/icomoon.ttf | Bin 1272 -> 0 bytes web-ui/app/fonts/icomoon.woff | Bin 1348 -> 0 bytes web-ui/app/images/LOADING-transparent.gif | Bin 16170 -> 0 bytes web-ui/app/images/fa-sent.svg | 15 - web-ui/app/images/favicon.png | Bin 592 -> 0 bytes web-ui/app/images/logo.svg | 30 - .../pixelated-symbol-blue-transparent-01.png | Bin 9075 -> 0 bytes web-ui/app/index.html | 113 -- web-ui/app/js/dispatchers/left_pane_dispatcher.js | 62 - .../app/js/dispatchers/middle_pane_dispatcher.js | 74 - web-ui/app/js/dispatchers/right_pane_dispatcher.js | 117 -- web-ui/app/js/features/features.js | 61 - web-ui/app/js/feedback/feedback_cache.js | 35 - web-ui/app/js/feedback/feedback_trigger.js | 39 - web-ui/app/js/foundation/initialize_foundation.js | 5 - web-ui/app/js/foundation/off_canvas.js | 46 - web-ui/app/js/helpers/browser.js | 36 - web-ui/app/js/helpers/contenttype.js | 184 -- web-ui/app/js/helpers/iterator.js | 60 - web-ui/app/js/helpers/monitored_ajax.js | 67 - web-ui/app/js/helpers/sanitizer.js | 126 -- web-ui/app/js/helpers/triggering.js | 29 - web-ui/app/js/helpers/view_helper.js | 163 -- web-ui/app/js/lib/highlightRegex.js | 127 -- web-ui/app/js/lib/html4-defs.js | 640 ------ web-ui/app/js/mail_list/domain/refresher.js | 43 - web-ui/app/js/mail_list/ui/mail_item_factory.js | 59 - .../app/js/mail_list/ui/mail_items/draft_item.js | 55 - .../mail_list/ui/mail_items/generic_mail_item.js | 97 - web-ui/app/js/mail_list/ui/mail_items/mail_item.js | 88 - web-ui/app/js/mail_list/ui/mail_items/sent_item.js | 61 - web-ui/app/js/mail_list/ui/mail_list.js | 182 -- .../mail_list_actions/ui/archive_many_trigger.js | 29 - .../app/js/mail_list_actions/ui/compose_trigger.js | 57 - .../js/mail_list_actions/ui/delete_many_trigger.js | 47 - .../js/mail_list_actions/ui/mail_list_actions.js | 93 - .../mail_list_actions/ui/mark_as_unread_trigger.js | 47 - .../ui/mark_many_as_read_trigger.js | 47 - .../js/mail_list_actions/ui/pagination_trigger.js | 66 - .../mail_list_actions/ui/recover_many_trigger.js | 47 - .../app/js/mail_list_actions/ui/refresh_trigger.js | 44 - .../ui/toggle_check_all_trigger.js | 49 - web-ui/app/js/mail_view/data/feedback_sender.js | 49 - web-ui/app/js/mail_view/data/mail_builder.js | 102 - web-ui/app/js/mail_view/data/mail_sender.js | 93 - web-ui/app/js/mail_view/ui/attachment_icon.js | 61 - web-ui/app/js/mail_view/ui/attachment_list.js | 210 -- web-ui/app/js/mail_view/ui/compose_box.js | 84 - web-ui/app/js/mail_view/ui/draft_box.js | 109 -- web-ui/app/js/mail_view/ui/draft_save_status.js | 42 - web-ui/app/js/mail_view/ui/feedback_box.js | 69 - web-ui/app/js/mail_view/ui/forward_box.js | 97 - web-ui/app/js/mail_view/ui/mail_actions.js | 84 - web-ui/app/js/mail_view/ui/mail_view.js | 255 --- .../app/js/mail_view/ui/no_mails_available_pane.js | 50 - .../js/mail_view/ui/no_message_selected_pane.js | 41 - web-ui/app/js/mail_view/ui/recipients/recipient.js | 112 -- .../app/js/mail_view/ui/recipients/recipients.js | 193 -- .../js/mail_view/ui/recipients/recipients_input.js | 180 -- .../mail_view/ui/recipients/recipients_iterator.js | 59 - web-ui/app/js/mail_view/ui/reply_box.js | 116 -- web-ui/app/js/mail_view/ui/reply_section.js | 129 -- web-ui/app/js/mail_view/ui/send_button.js | 130 -- web-ui/app/js/main.js | 84 - web-ui/app/js/mixins/with_auto_refresh.js | 47 - web-ui/app/js/mixins/with_compose_inline.js | 84 - .../app/js/mixins/with_enable_disable_on_event.js | 48 - web-ui/app/js/mixins/with_feature_toggle.js | 40 - web-ui/app/js/mixins/with_hide_and_show.js | 31 - web-ui/app/js/mixins/with_mail_edit_base.js | 263 --- web-ui/app/js/mixins/with_mail_sandbox.js | 80 - web-ui/app/js/mixins/with_mail_tagging.js | 69 - web-ui/app/js/monkey_patching/all.js | 17 - web-ui/app/js/monkey_patching/array.js | 27 - web-ui/app/js/monkey_patching/post_message.js | 32 - web-ui/app/js/page/default.js | 146 -- web-ui/app/js/page/events.js | 222 --- web-ui/app/js/page/logout.js | 43 - web-ui/app/js/page/logout_shortcut.js | 33 - web-ui/app/js/page/pane_contract_expand.js | 51 - web-ui/app/js/page/pix_logo.js | 62 - web-ui/app/js/page/router.js | 71 - web-ui/app/js/page/router/url_params.js | 57 - web-ui/app/js/page/unread_count_title.js | 53 - web-ui/app/js/page/version.js | 41 - web-ui/app/js/sandbox.js | 11 - web-ui/app/js/search/results_highlighter.js | 97 - web-ui/app/js/search/search_trigger.js | 81 - web-ui/app/js/services/delete_service.js | 59 - web-ui/app/js/services/mail_service.js | 335 ---- web-ui/app/js/services/model/mail.js | 126 -- web-ui/app/js/services/recover_service.js | 38 - web-ui/app/js/style_guide/main.js | 33 - web-ui/app/js/tags/data/tags.js | 66 - web-ui/app/js/tags/ui/tag.js | 154 -- web-ui/app/js/tags/ui/tag_base.js | 68 - web-ui/app/js/tags/ui/tag_list.js | 105 - web-ui/app/js/user_alerts/ui/user_alerts.js | 57 - web-ui/app/js/user_settings/data/user_settings.js | 52 - .../app/js/user_settings/ui/user_settings_box.js | 77 - .../app/js/user_settings/ui/user_settings_icon.js | 57 - web-ui/app/js/views/i18n.js | 62 - web-ui/app/js/views/recipientListFormatter.js | 33 - web-ui/app/js/views/templates.js | 85 - web-ui/app/locales/en_US/translation.json | 72 - web-ui/app/locales/pt_BR/translation.json | 72 - web-ui/app/locales/sv_SE/translation.json | 42 - web-ui/app/robots.txt | 3 - web-ui/app/sandbox.html | 16 - web-ui/app/scss/_mixins.scss | 71 - web-ui/app/scss/_others.scss | 72 - web-ui/app/scss/base/_colors.scss | 64 - web-ui/app/scss/base/_fonts.scss | 68 - web-ui/app/scss/base/_scaffolding.scss | 10 - web-ui/app/scss/mixins/_position-helpers.scss | 9 - web-ui/app/scss/mixins/_tags.scss | 110 -- web-ui/app/scss/sandbox.scss | 27 - web-ui/app/scss/style.scss | 39 - .../scss/templates/_no-content-placeholder.scss | 5 - web-ui/app/scss/templates/_unread-count.scss | 14 - web-ui/app/scss/vendor/_customfont.scss | 9 - web-ui/app/scss/vendor/_foundation.scss | 2066 -------------------- web-ui/app/scss/vendor/_reset.scss | 421 ---- web-ui/app/scss/vendor/_scut.scss | 1518 -------------- web-ui/app/scss/views/_action-bar.scss | 159 -- web-ui/app/scss/views/_close-button.scss | 22 - web-ui/app/scss/views/_compose-button.scss | 27 - web-ui/app/scss/views/_compose-view.scss | 451 ----- web-ui/app/scss/views/_mail-list.scss | 124 -- web-ui/app/scss/views/_message-panel.scss | 26 - web-ui/app/scss/views/_navigation.scss | 589 ------ web-ui/app/scss/views/_no-mails-available.scss | 3 - web-ui/app/scss/views/_no-message-selected.scss | 14 - web-ui/app/scss/views/_read-view.scss | 165 -- web-ui/app/scss/views/_security-labels.scss | 67 - web-ui/app/templates/compose/attachment_item.hbs | 4 - .../templates/compose/attachment_upload_item.hbs | 5 - web-ui/app/templates/compose/attachments_list.hbs | 14 - web-ui/app/templates/compose/compose_box.hbs | 32 - web-ui/app/templates/compose/feedback_box.hbs | 18 - web-ui/app/templates/compose/fixed_recipient.hbs | 8 - web-ui/app/templates/compose/inline_box.hbs | 20 - .../app/templates/compose/no_mails_available.hbs | 7 - .../app/templates/compose/no_message_selected.hbs | 3 - web-ui/app/templates/compose/recipient_input.hbs | 1 - web-ui/app/templates/compose/recipients.hbs | 33 - web-ui/app/templates/compose/reply_section.hbs | 6 - .../templates/compose/upload_attachment_failed.hbs | 6 - web-ui/app/templates/feedback/feedback_trigger.hbs | 8 - web-ui/app/templates/mail_actions/actions_box.hbs | 7 - .../app/templates/mail_actions/compose_trigger.hbs | 3 - .../templates/mail_actions/pagination_trigger.hbs | 3 - .../app/templates/mail_actions/refresh_trigger.hbs | 3 - .../templates/mail_actions/trash_actions_box.hbs | 5 - web-ui/app/templates/mails/draft.hbs | 41 - web-ui/app/templates/mails/full_view.hbs | 83 - web-ui/app/templates/mails/mail_actions.hbs | 6 - web-ui/app/templates/mails/sent.hbs | 36 - web-ui/app/templates/mails/single.hbs | 28 - web-ui/app/templates/mails/trash.hbs | 32 - web-ui/app/templates/page/logout.hbs | 9 - web-ui/app/templates/page/logout_shortcut.hbs | 6 - web-ui/app/templates/page/user_settings_box.hbs | 10 - web-ui/app/templates/page/user_settings_icon.hbs | 8 - web-ui/app/templates/page/version.hbs | 2 - web-ui/app/templates/search/search_trigger.hbs | 3 - web-ui/app/templates/tags/shortcut.hbs | 9 - web-ui/app/templates/tags/tag.hbs | 3 - web-ui/app/templates/tags/tag_inner.hbs | 4 - web-ui/app/templates/tags/tag_list.hbs | 6 - web-ui/app/templates/user_alerts/message.hbs | 1 - web-ui/config/add_git_version.sh | 2 +- web-ui/config/buildoptions.js | 4 +- web-ui/config/compass.rb | 8 +- web-ui/config/control-tower.yml | 2 +- web-ui/config/imagemin.js | 2 +- web-ui/config/package.sh | 32 +- web-ui/karma.conf.js | 54 +- web-ui/package.json | 12 +- web-ui/public/404.html | 157 ++ web-ui/public/favicon.ico | 0 web-ui/public/fonts/OpenSans-Bold.woff | Bin 0 -> 14504 bytes web-ui/public/fonts/OpenSans-BoldItalic.woff | Bin 0 -> 15488 bytes web-ui/public/fonts/OpenSans-Extrabold.woff | Bin 0 -> 15312 bytes web-ui/public/fonts/OpenSans-ExtraboldItalic.woff | Bin 0 -> 15932 bytes web-ui/public/fonts/OpenSans-Italic.woff | Bin 0 -> 15768 bytes web-ui/public/fonts/OpenSans-Light.woff | Bin 0 -> 15048 bytes web-ui/public/fonts/OpenSans-Semibold.woff | Bin 0 -> 15236 bytes web-ui/public/fonts/OpenSans-SemiboldItalic.woff | Bin 0 -> 15736 bytes web-ui/public/fonts/OpenSans.woff | Bin 0 -> 14604 bytes web-ui/public/fonts/OpenSansLight-Italic.woff | Bin 0 -> 15956 bytes web-ui/public/fonts/icomoon.ttf | Bin 0 -> 1272 bytes web-ui/public/fonts/icomoon.woff | Bin 0 -> 1348 bytes web-ui/public/images/LOADING-transparent.gif | Bin 0 -> 16170 bytes web-ui/public/images/fa-sent.svg | 15 + web-ui/public/images/favicon.png | Bin 0 -> 592 bytes web-ui/public/images/logo.svg | 30 + .../pixelated-symbol-blue-transparent-01.png | Bin 0 -> 9075 bytes web-ui/public/index.html | 113 ++ .../public/js/dispatchers/left_pane_dispatcher.js | 62 + .../js/dispatchers/middle_pane_dispatcher.js | 74 + .../public/js/dispatchers/right_pane_dispatcher.js | 117 ++ web-ui/public/js/features/features.js | 61 + web-ui/public/js/feedback/feedback_cache.js | 35 + web-ui/public/js/feedback/feedback_trigger.js | 39 + .../public/js/foundation/initialize_foundation.js | 5 + web-ui/public/js/foundation/off_canvas.js | 46 + web-ui/public/js/helpers/browser.js | 36 + web-ui/public/js/helpers/contenttype.js | 184 ++ web-ui/public/js/helpers/iterator.js | 60 + web-ui/public/js/helpers/monitored_ajax.js | 67 + web-ui/public/js/helpers/sanitizer.js | 126 ++ web-ui/public/js/helpers/triggering.js | 29 + web-ui/public/js/helpers/view_helper.js | 163 ++ web-ui/public/js/lib/highlightRegex.js | 127 ++ web-ui/public/js/lib/html4-defs.js | 640 ++++++ web-ui/public/js/mail_list/domain/refresher.js | 43 + web-ui/public/js/mail_list/ui/mail_item_factory.js | 59 + .../js/mail_list/ui/mail_items/draft_item.js | 55 + .../mail_list/ui/mail_items/generic_mail_item.js | 97 + .../public/js/mail_list/ui/mail_items/mail_item.js | 88 + .../public/js/mail_list/ui/mail_items/sent_item.js | 61 + web-ui/public/js/mail_list/ui/mail_list.js | 182 ++ .../mail_list_actions/ui/archive_many_trigger.js | 29 + .../js/mail_list_actions/ui/compose_trigger.js | 57 + .../js/mail_list_actions/ui/delete_many_trigger.js | 47 + .../js/mail_list_actions/ui/mail_list_actions.js | 93 + .../mail_list_actions/ui/mark_as_unread_trigger.js | 47 + .../ui/mark_many_as_read_trigger.js | 47 + .../js/mail_list_actions/ui/pagination_trigger.js | 66 + .../mail_list_actions/ui/recover_many_trigger.js | 47 + .../js/mail_list_actions/ui/refresh_trigger.js | 44 + .../ui/toggle_check_all_trigger.js | 49 + web-ui/public/js/mail_view/data/feedback_sender.js | 49 + web-ui/public/js/mail_view/data/mail_builder.js | 102 + web-ui/public/js/mail_view/data/mail_sender.js | 93 + web-ui/public/js/mail_view/ui/attachment_icon.js | 61 + web-ui/public/js/mail_view/ui/attachment_list.js | 210 ++ web-ui/public/js/mail_view/ui/compose_box.js | 84 + web-ui/public/js/mail_view/ui/draft_box.js | 109 ++ web-ui/public/js/mail_view/ui/draft_save_status.js | 42 + web-ui/public/js/mail_view/ui/feedback_box.js | 69 + web-ui/public/js/mail_view/ui/forward_box.js | 97 + web-ui/public/js/mail_view/ui/mail_actions.js | 84 + web-ui/public/js/mail_view/ui/mail_view.js | 255 +++ .../js/mail_view/ui/no_mails_available_pane.js | 50 + .../js/mail_view/ui/no_message_selected_pane.js | 41 + .../public/js/mail_view/ui/recipients/recipient.js | 112 ++ .../js/mail_view/ui/recipients/recipients.js | 193 ++ .../js/mail_view/ui/recipients/recipients_input.js | 180 ++ .../mail_view/ui/recipients/recipients_iterator.js | 59 + web-ui/public/js/mail_view/ui/reply_box.js | 116 ++ web-ui/public/js/mail_view/ui/reply_section.js | 129 ++ web-ui/public/js/mail_view/ui/send_button.js | 130 ++ web-ui/public/js/main.js | 84 + web-ui/public/js/mixins/with_auto_refresh.js | 47 + web-ui/public/js/mixins/with_compose_inline.js | 84 + .../js/mixins/with_enable_disable_on_event.js | 48 + web-ui/public/js/mixins/with_feature_toggle.js | 40 + web-ui/public/js/mixins/with_hide_and_show.js | 31 + web-ui/public/js/mixins/with_mail_edit_base.js | 263 +++ web-ui/public/js/mixins/with_mail_sandbox.js | 80 + web-ui/public/js/mixins/with_mail_tagging.js | 69 + web-ui/public/js/monkey_patching/all.js | 17 + web-ui/public/js/monkey_patching/array.js | 27 + web-ui/public/js/monkey_patching/post_message.js | 32 + web-ui/public/js/page/default.js | 146 ++ web-ui/public/js/page/events.js | 222 +++ web-ui/public/js/page/logout.js | 43 + web-ui/public/js/page/logout_shortcut.js | 33 + web-ui/public/js/page/pane_contract_expand.js | 51 + web-ui/public/js/page/pix_logo.js | 62 + web-ui/public/js/page/router.js | 71 + web-ui/public/js/page/router/url_params.js | 57 + web-ui/public/js/page/unread_count_title.js | 53 + web-ui/public/js/page/version.js | 41 + web-ui/public/js/sandbox.js | 11 + web-ui/public/js/search/results_highlighter.js | 97 + web-ui/public/js/search/search_trigger.js | 81 + web-ui/public/js/services/delete_service.js | 59 + web-ui/public/js/services/mail_service.js | 335 ++++ web-ui/public/js/services/model/mail.js | 126 ++ web-ui/public/js/services/recover_service.js | 38 + web-ui/public/js/style_guide/main.js | 33 + web-ui/public/js/tags/data/tags.js | 66 + web-ui/public/js/tags/ui/tag.js | 154 ++ web-ui/public/js/tags/ui/tag_base.js | 68 + web-ui/public/js/tags/ui/tag_list.js | 105 + web-ui/public/js/user_alerts/ui/user_alerts.js | 57 + .../public/js/user_settings/data/user_settings.js | 52 + .../js/user_settings/ui/user_settings_box.js | 77 + .../js/user_settings/ui/user_settings_icon.js | 57 + web-ui/public/js/views/i18n.js | 62 + web-ui/public/js/views/recipientListFormatter.js | 33 + web-ui/public/js/views/templates.js | 85 + web-ui/public/locales/en_US/translation.json | 72 + web-ui/public/locales/pt_BR/translation.json | 72 + web-ui/public/locales/sv_SE/translation.json | 42 + web-ui/public/robots.txt | 3 + web-ui/public/sandbox.html | 16 + web-ui/public/scss/_mixins.scss | 71 + web-ui/public/scss/_others.scss | 72 + web-ui/public/scss/base/_colors.scss | 64 + web-ui/public/scss/base/_fonts.scss | 68 + web-ui/public/scss/base/_scaffolding.scss | 10 + web-ui/public/scss/mixins/_position-helpers.scss | 9 + web-ui/public/scss/mixins/_tags.scss | 110 ++ web-ui/public/scss/sandbox.scss | 27 + web-ui/public/scss/style.scss | 39 + .../scss/templates/_no-content-placeholder.scss | 5 + web-ui/public/scss/templates/_unread-count.scss | 14 + web-ui/public/scss/vendor/_customfont.scss | 9 + web-ui/public/scss/vendor/_foundation.scss | 2066 ++++++++++++++++++++ web-ui/public/scss/vendor/_reset.scss | 421 ++++ web-ui/public/scss/vendor/_scut.scss | 1518 ++++++++++++++ web-ui/public/scss/views/_action-bar.scss | 159 ++ web-ui/public/scss/views/_close-button.scss | 22 + web-ui/public/scss/views/_compose-button.scss | 27 + web-ui/public/scss/views/_compose-view.scss | 451 +++++ web-ui/public/scss/views/_mail-list.scss | 124 ++ web-ui/public/scss/views/_message-panel.scss | 26 + web-ui/public/scss/views/_navigation.scss | 589 ++++++ web-ui/public/scss/views/_no-mails-available.scss | 3 + web-ui/public/scss/views/_no-message-selected.scss | 14 + web-ui/public/scss/views/_read-view.scss | 165 ++ web-ui/public/scss/views/_security-labels.scss | 67 + .../public/templates/compose/attachment_item.hbs | 4 + .../templates/compose/attachment_upload_item.hbs | 5 + .../public/templates/compose/attachments_list.hbs | 14 + web-ui/public/templates/compose/compose_box.hbs | 32 + web-ui/public/templates/compose/feedback_box.hbs | 18 + .../public/templates/compose/fixed_recipient.hbs | 8 + web-ui/public/templates/compose/inline_box.hbs | 20 + .../templates/compose/no_mails_available.hbs | 7 + .../templates/compose/no_message_selected.hbs | 3 + .../public/templates/compose/recipient_input.hbs | 1 + web-ui/public/templates/compose/recipients.hbs | 33 + web-ui/public/templates/compose/reply_section.hbs | 6 + .../templates/compose/upload_attachment_failed.hbs | 6 + .../public/templates/feedback/feedback_trigger.hbs | 8 + .../public/templates/mail_actions/actions_box.hbs | 7 + .../templates/mail_actions/compose_trigger.hbs | 3 + .../templates/mail_actions/pagination_trigger.hbs | 3 + .../templates/mail_actions/refresh_trigger.hbs | 3 + .../templates/mail_actions/trash_actions_box.hbs | 5 + web-ui/public/templates/mails/draft.hbs | 41 + web-ui/public/templates/mails/full_view.hbs | 83 + web-ui/public/templates/mails/mail_actions.hbs | 6 + web-ui/public/templates/mails/sent.hbs | 36 + web-ui/public/templates/mails/single.hbs | 28 + web-ui/public/templates/mails/trash.hbs | 32 + web-ui/public/templates/page/logout.hbs | 9 + web-ui/public/templates/page/logout_shortcut.hbs | 6 + web-ui/public/templates/page/user_settings_box.hbs | 10 + .../public/templates/page/user_settings_icon.hbs | 8 + web-ui/public/templates/page/version.hbs | 2 + web-ui/public/templates/search/search_trigger.hbs | 3 + web-ui/public/templates/tags/shortcut.hbs | 9 + web-ui/public/templates/tags/tag.hbs | 3 + web-ui/public/templates/tags/tag_inner.hbs | 4 + web-ui/public/templates/tags/tag_list.hbs | 6 + web-ui/public/templates/user_alerts/message.hbs | 1 + web-ui/test/test-main.js | 54 +- 389 files changed, 15742 insertions(+), 15711 deletions(-) create mode 100644 service/test/integration/test_static_files.py delete mode 100644 web-ui/app/404.html delete mode 100644 web-ui/app/favicon.ico delete mode 100644 web-ui/app/fonts/OpenSans-Bold.woff delete mode 100644 web-ui/app/fonts/OpenSans-BoldItalic.woff delete mode 100644 web-ui/app/fonts/OpenSans-Extrabold.woff delete mode 100644 web-ui/app/fonts/OpenSans-ExtraboldItalic.woff delete mode 100644 web-ui/app/fonts/OpenSans-Italic.woff delete mode 100644 web-ui/app/fonts/OpenSans-Light.woff delete mode 100644 web-ui/app/fonts/OpenSans-Semibold.woff delete mode 100644 web-ui/app/fonts/OpenSans-SemiboldItalic.woff delete mode 100644 web-ui/app/fonts/OpenSans.woff delete mode 100644 web-ui/app/fonts/OpenSansLight-Italic.woff delete mode 100644 web-ui/app/fonts/icomoon.ttf delete mode 100644 web-ui/app/fonts/icomoon.woff delete mode 100644 web-ui/app/images/LOADING-transparent.gif delete mode 100644 web-ui/app/images/fa-sent.svg delete mode 100644 web-ui/app/images/favicon.png delete mode 100644 web-ui/app/images/logo.svg delete mode 100644 web-ui/app/images/pixelated-symbol-blue-transparent-01.png delete mode 100644 web-ui/app/index.html delete mode 100644 web-ui/app/js/dispatchers/left_pane_dispatcher.js delete mode 100644 web-ui/app/js/dispatchers/middle_pane_dispatcher.js delete mode 100644 web-ui/app/js/dispatchers/right_pane_dispatcher.js delete mode 100644 web-ui/app/js/features/features.js delete mode 100644 web-ui/app/js/feedback/feedback_cache.js delete mode 100644 web-ui/app/js/feedback/feedback_trigger.js delete mode 100644 web-ui/app/js/foundation/initialize_foundation.js delete mode 100644 web-ui/app/js/foundation/off_canvas.js delete mode 100644 web-ui/app/js/helpers/browser.js delete mode 100644 web-ui/app/js/helpers/contenttype.js delete mode 100644 web-ui/app/js/helpers/iterator.js delete mode 100644 web-ui/app/js/helpers/monitored_ajax.js delete mode 100644 web-ui/app/js/helpers/sanitizer.js delete mode 100644 web-ui/app/js/helpers/triggering.js delete mode 100644 web-ui/app/js/helpers/view_helper.js delete mode 100644 web-ui/app/js/lib/highlightRegex.js delete mode 100644 web-ui/app/js/lib/html4-defs.js delete mode 100644 web-ui/app/js/mail_list/domain/refresher.js delete mode 100644 web-ui/app/js/mail_list/ui/mail_item_factory.js delete mode 100644 web-ui/app/js/mail_list/ui/mail_items/draft_item.js delete mode 100644 web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.js delete mode 100644 web-ui/app/js/mail_list/ui/mail_items/mail_item.js delete mode 100644 web-ui/app/js/mail_list/ui/mail_items/sent_item.js delete mode 100644 web-ui/app/js/mail_list/ui/mail_list.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/archive_many_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/compose_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/mail_list_actions.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/pagination_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/refresh_trigger.js delete mode 100644 web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js delete mode 100644 web-ui/app/js/mail_view/data/feedback_sender.js delete mode 100644 web-ui/app/js/mail_view/data/mail_builder.js delete mode 100644 web-ui/app/js/mail_view/data/mail_sender.js delete mode 100644 web-ui/app/js/mail_view/ui/attachment_icon.js delete mode 100644 web-ui/app/js/mail_view/ui/attachment_list.js delete mode 100644 web-ui/app/js/mail_view/ui/compose_box.js delete mode 100644 web-ui/app/js/mail_view/ui/draft_box.js delete mode 100644 web-ui/app/js/mail_view/ui/draft_save_status.js delete mode 100644 web-ui/app/js/mail_view/ui/feedback_box.js delete mode 100644 web-ui/app/js/mail_view/ui/forward_box.js delete mode 100644 web-ui/app/js/mail_view/ui/mail_actions.js delete mode 100644 web-ui/app/js/mail_view/ui/mail_view.js delete mode 100644 web-ui/app/js/mail_view/ui/no_mails_available_pane.js delete mode 100644 web-ui/app/js/mail_view/ui/no_message_selected_pane.js delete mode 100644 web-ui/app/js/mail_view/ui/recipients/recipient.js delete mode 100644 web-ui/app/js/mail_view/ui/recipients/recipients.js delete mode 100644 web-ui/app/js/mail_view/ui/recipients/recipients_input.js delete mode 100644 web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js delete mode 100644 web-ui/app/js/mail_view/ui/reply_box.js delete mode 100644 web-ui/app/js/mail_view/ui/reply_section.js delete mode 100644 web-ui/app/js/mail_view/ui/send_button.js delete mode 100644 web-ui/app/js/main.js delete mode 100644 web-ui/app/js/mixins/with_auto_refresh.js delete mode 100644 web-ui/app/js/mixins/with_compose_inline.js delete mode 100644 web-ui/app/js/mixins/with_enable_disable_on_event.js delete mode 100644 web-ui/app/js/mixins/with_feature_toggle.js delete mode 100644 web-ui/app/js/mixins/with_hide_and_show.js delete mode 100644 web-ui/app/js/mixins/with_mail_edit_base.js delete mode 100644 web-ui/app/js/mixins/with_mail_sandbox.js delete mode 100644 web-ui/app/js/mixins/with_mail_tagging.js delete mode 100644 web-ui/app/js/monkey_patching/all.js delete mode 100644 web-ui/app/js/monkey_patching/array.js delete mode 100644 web-ui/app/js/monkey_patching/post_message.js delete mode 100644 web-ui/app/js/page/default.js delete mode 100644 web-ui/app/js/page/events.js delete mode 100644 web-ui/app/js/page/logout.js delete mode 100644 web-ui/app/js/page/logout_shortcut.js delete mode 100644 web-ui/app/js/page/pane_contract_expand.js delete mode 100644 web-ui/app/js/page/pix_logo.js delete mode 100644 web-ui/app/js/page/router.js delete mode 100644 web-ui/app/js/page/router/url_params.js delete mode 100644 web-ui/app/js/page/unread_count_title.js delete mode 100644 web-ui/app/js/page/version.js delete mode 100644 web-ui/app/js/sandbox.js delete mode 100644 web-ui/app/js/search/results_highlighter.js delete mode 100644 web-ui/app/js/search/search_trigger.js delete mode 100644 web-ui/app/js/services/delete_service.js delete mode 100644 web-ui/app/js/services/mail_service.js delete mode 100644 web-ui/app/js/services/model/mail.js delete mode 100644 web-ui/app/js/services/recover_service.js delete mode 100644 web-ui/app/js/style_guide/main.js delete mode 100644 web-ui/app/js/tags/data/tags.js delete mode 100644 web-ui/app/js/tags/ui/tag.js delete mode 100644 web-ui/app/js/tags/ui/tag_base.js delete mode 100644 web-ui/app/js/tags/ui/tag_list.js delete mode 100644 web-ui/app/js/user_alerts/ui/user_alerts.js delete mode 100644 web-ui/app/js/user_settings/data/user_settings.js delete mode 100644 web-ui/app/js/user_settings/ui/user_settings_box.js delete mode 100644 web-ui/app/js/user_settings/ui/user_settings_icon.js delete mode 100644 web-ui/app/js/views/i18n.js delete mode 100644 web-ui/app/js/views/recipientListFormatter.js delete mode 100644 web-ui/app/js/views/templates.js delete mode 100644 web-ui/app/locales/en_US/translation.json delete mode 100644 web-ui/app/locales/pt_BR/translation.json delete mode 100644 web-ui/app/locales/sv_SE/translation.json delete mode 100644 web-ui/app/robots.txt delete mode 100644 web-ui/app/sandbox.html delete mode 100644 web-ui/app/scss/_mixins.scss delete mode 100644 web-ui/app/scss/_others.scss delete mode 100644 web-ui/app/scss/base/_colors.scss delete mode 100644 web-ui/app/scss/base/_fonts.scss delete mode 100644 web-ui/app/scss/base/_scaffolding.scss delete mode 100644 web-ui/app/scss/mixins/_position-helpers.scss delete mode 100644 web-ui/app/scss/mixins/_tags.scss delete mode 100644 web-ui/app/scss/sandbox.scss delete mode 100644 web-ui/app/scss/style.scss delete mode 100644 web-ui/app/scss/templates/_no-content-placeholder.scss delete mode 100644 web-ui/app/scss/templates/_unread-count.scss delete mode 100644 web-ui/app/scss/vendor/_customfont.scss delete mode 100644 web-ui/app/scss/vendor/_foundation.scss delete mode 100644 web-ui/app/scss/vendor/_reset.scss delete mode 100644 web-ui/app/scss/vendor/_scut.scss delete mode 100644 web-ui/app/scss/views/_action-bar.scss delete mode 100644 web-ui/app/scss/views/_close-button.scss delete mode 100644 web-ui/app/scss/views/_compose-button.scss delete mode 100644 web-ui/app/scss/views/_compose-view.scss delete mode 100644 web-ui/app/scss/views/_mail-list.scss delete mode 100644 web-ui/app/scss/views/_message-panel.scss delete mode 100644 web-ui/app/scss/views/_navigation.scss delete mode 100644 web-ui/app/scss/views/_no-mails-available.scss delete mode 100644 web-ui/app/scss/views/_no-message-selected.scss delete mode 100644 web-ui/app/scss/views/_read-view.scss delete mode 100644 web-ui/app/scss/views/_security-labels.scss delete mode 100644 web-ui/app/templates/compose/attachment_item.hbs delete mode 100644 web-ui/app/templates/compose/attachment_upload_item.hbs delete mode 100644 web-ui/app/templates/compose/attachments_list.hbs delete mode 100644 web-ui/app/templates/compose/compose_box.hbs delete mode 100644 web-ui/app/templates/compose/feedback_box.hbs delete mode 100644 web-ui/app/templates/compose/fixed_recipient.hbs delete mode 100644 web-ui/app/templates/compose/inline_box.hbs delete mode 100644 web-ui/app/templates/compose/no_mails_available.hbs delete mode 100644 web-ui/app/templates/compose/no_message_selected.hbs delete mode 100644 web-ui/app/templates/compose/recipient_input.hbs delete mode 100644 web-ui/app/templates/compose/recipients.hbs delete mode 100644 web-ui/app/templates/compose/reply_section.hbs delete mode 100644 web-ui/app/templates/compose/upload_attachment_failed.hbs delete mode 100644 web-ui/app/templates/feedback/feedback_trigger.hbs delete mode 100644 web-ui/app/templates/mail_actions/actions_box.hbs delete mode 100644 web-ui/app/templates/mail_actions/compose_trigger.hbs delete mode 100644 web-ui/app/templates/mail_actions/pagination_trigger.hbs delete mode 100644 web-ui/app/templates/mail_actions/refresh_trigger.hbs delete mode 100644 web-ui/app/templates/mail_actions/trash_actions_box.hbs delete mode 100644 web-ui/app/templates/mails/draft.hbs delete mode 100644 web-ui/app/templates/mails/full_view.hbs delete mode 100644 web-ui/app/templates/mails/mail_actions.hbs delete mode 100644 web-ui/app/templates/mails/sent.hbs delete mode 100644 web-ui/app/templates/mails/single.hbs delete mode 100644 web-ui/app/templates/mails/trash.hbs delete mode 100644 web-ui/app/templates/page/logout.hbs delete mode 100644 web-ui/app/templates/page/logout_shortcut.hbs delete mode 100644 web-ui/app/templates/page/user_settings_box.hbs delete mode 100644 web-ui/app/templates/page/user_settings_icon.hbs delete mode 100644 web-ui/app/templates/page/version.hbs delete mode 100644 web-ui/app/templates/search/search_trigger.hbs delete mode 100644 web-ui/app/templates/tags/shortcut.hbs delete mode 100644 web-ui/app/templates/tags/tag.hbs delete mode 100644 web-ui/app/templates/tags/tag_inner.hbs delete mode 100644 web-ui/app/templates/tags/tag_list.hbs delete mode 100644 web-ui/app/templates/user_alerts/message.hbs create mode 100644 web-ui/public/404.html create mode 100644 web-ui/public/favicon.ico create mode 100644 web-ui/public/fonts/OpenSans-Bold.woff create mode 100644 web-ui/public/fonts/OpenSans-BoldItalic.woff create mode 100644 web-ui/public/fonts/OpenSans-Extrabold.woff create mode 100644 web-ui/public/fonts/OpenSans-ExtraboldItalic.woff create mode 100644 web-ui/public/fonts/OpenSans-Italic.woff create mode 100644 web-ui/public/fonts/OpenSans-Light.woff create mode 100644 web-ui/public/fonts/OpenSans-Semibold.woff create mode 100644 web-ui/public/fonts/OpenSans-SemiboldItalic.woff create mode 100644 web-ui/public/fonts/OpenSans.woff create mode 100644 web-ui/public/fonts/OpenSansLight-Italic.woff create mode 100644 web-ui/public/fonts/icomoon.ttf create mode 100644 web-ui/public/fonts/icomoon.woff create mode 100644 web-ui/public/images/LOADING-transparent.gif create mode 100644 web-ui/public/images/fa-sent.svg create mode 100644 web-ui/public/images/favicon.png create mode 100644 web-ui/public/images/logo.svg create mode 100644 web-ui/public/images/pixelated-symbol-blue-transparent-01.png create mode 100644 web-ui/public/index.html create mode 100644 web-ui/public/js/dispatchers/left_pane_dispatcher.js create mode 100644 web-ui/public/js/dispatchers/middle_pane_dispatcher.js create mode 100644 web-ui/public/js/dispatchers/right_pane_dispatcher.js create mode 100644 web-ui/public/js/features/features.js create mode 100644 web-ui/public/js/feedback/feedback_cache.js create mode 100644 web-ui/public/js/feedback/feedback_trigger.js create mode 100644 web-ui/public/js/foundation/initialize_foundation.js create mode 100644 web-ui/public/js/foundation/off_canvas.js create mode 100644 web-ui/public/js/helpers/browser.js create mode 100644 web-ui/public/js/helpers/contenttype.js create mode 100644 web-ui/public/js/helpers/iterator.js create mode 100644 web-ui/public/js/helpers/monitored_ajax.js create mode 100644 web-ui/public/js/helpers/sanitizer.js create mode 100644 web-ui/public/js/helpers/triggering.js create mode 100644 web-ui/public/js/helpers/view_helper.js create mode 100644 web-ui/public/js/lib/highlightRegex.js create mode 100644 web-ui/public/js/lib/html4-defs.js create mode 100644 web-ui/public/js/mail_list/domain/refresher.js create mode 100644 web-ui/public/js/mail_list/ui/mail_item_factory.js create mode 100644 web-ui/public/js/mail_list/ui/mail_items/draft_item.js create mode 100644 web-ui/public/js/mail_list/ui/mail_items/generic_mail_item.js create mode 100644 web-ui/public/js/mail_list/ui/mail_items/mail_item.js create mode 100644 web-ui/public/js/mail_list/ui/mail_items/sent_item.js create mode 100644 web-ui/public/js/mail_list/ui/mail_list.js create mode 100644 web-ui/public/js/mail_list_actions/ui/archive_many_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/compose_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/delete_many_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/mail_list_actions.js create mode 100644 web-ui/public/js/mail_list_actions/ui/mark_as_unread_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/pagination_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/recover_many_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/refresh_trigger.js create mode 100644 web-ui/public/js/mail_list_actions/ui/toggle_check_all_trigger.js create mode 100644 web-ui/public/js/mail_view/data/feedback_sender.js create mode 100644 web-ui/public/js/mail_view/data/mail_builder.js create mode 100644 web-ui/public/js/mail_view/data/mail_sender.js create mode 100644 web-ui/public/js/mail_view/ui/attachment_icon.js create mode 100644 web-ui/public/js/mail_view/ui/attachment_list.js create mode 100644 web-ui/public/js/mail_view/ui/compose_box.js create mode 100644 web-ui/public/js/mail_view/ui/draft_box.js create mode 100644 web-ui/public/js/mail_view/ui/draft_save_status.js create mode 100644 web-ui/public/js/mail_view/ui/feedback_box.js create mode 100644 web-ui/public/js/mail_view/ui/forward_box.js create mode 100644 web-ui/public/js/mail_view/ui/mail_actions.js create mode 100644 web-ui/public/js/mail_view/ui/mail_view.js create mode 100644 web-ui/public/js/mail_view/ui/no_mails_available_pane.js create mode 100644 web-ui/public/js/mail_view/ui/no_message_selected_pane.js create mode 100644 web-ui/public/js/mail_view/ui/recipients/recipient.js create mode 100644 web-ui/public/js/mail_view/ui/recipients/recipients.js create mode 100644 web-ui/public/js/mail_view/ui/recipients/recipients_input.js create mode 100644 web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js create mode 100644 web-ui/public/js/mail_view/ui/reply_box.js create mode 100644 web-ui/public/js/mail_view/ui/reply_section.js create mode 100644 web-ui/public/js/mail_view/ui/send_button.js create mode 100644 web-ui/public/js/main.js create mode 100644 web-ui/public/js/mixins/with_auto_refresh.js create mode 100644 web-ui/public/js/mixins/with_compose_inline.js create mode 100644 web-ui/public/js/mixins/with_enable_disable_on_event.js create mode 100644 web-ui/public/js/mixins/with_feature_toggle.js create mode 100644 web-ui/public/js/mixins/with_hide_and_show.js create mode 100644 web-ui/public/js/mixins/with_mail_edit_base.js create mode 100644 web-ui/public/js/mixins/with_mail_sandbox.js create mode 100644 web-ui/public/js/mixins/with_mail_tagging.js create mode 100644 web-ui/public/js/monkey_patching/all.js create mode 100644 web-ui/public/js/monkey_patching/array.js create mode 100644 web-ui/public/js/monkey_patching/post_message.js create mode 100644 web-ui/public/js/page/default.js create mode 100644 web-ui/public/js/page/events.js create mode 100644 web-ui/public/js/page/logout.js create mode 100644 web-ui/public/js/page/logout_shortcut.js create mode 100644 web-ui/public/js/page/pane_contract_expand.js create mode 100644 web-ui/public/js/page/pix_logo.js create mode 100644 web-ui/public/js/page/router.js create mode 100644 web-ui/public/js/page/router/url_params.js create mode 100644 web-ui/public/js/page/unread_count_title.js create mode 100644 web-ui/public/js/page/version.js create mode 100644 web-ui/public/js/sandbox.js create mode 100644 web-ui/public/js/search/results_highlighter.js create mode 100644 web-ui/public/js/search/search_trigger.js create mode 100644 web-ui/public/js/services/delete_service.js create mode 100644 web-ui/public/js/services/mail_service.js create mode 100644 web-ui/public/js/services/model/mail.js create mode 100644 web-ui/public/js/services/recover_service.js create mode 100644 web-ui/public/js/style_guide/main.js create mode 100644 web-ui/public/js/tags/data/tags.js create mode 100644 web-ui/public/js/tags/ui/tag.js create mode 100644 web-ui/public/js/tags/ui/tag_base.js create mode 100644 web-ui/public/js/tags/ui/tag_list.js create mode 100644 web-ui/public/js/user_alerts/ui/user_alerts.js create mode 100644 web-ui/public/js/user_settings/data/user_settings.js create mode 100644 web-ui/public/js/user_settings/ui/user_settings_box.js create mode 100644 web-ui/public/js/user_settings/ui/user_settings_icon.js create mode 100644 web-ui/public/js/views/i18n.js create mode 100644 web-ui/public/js/views/recipientListFormatter.js create mode 100644 web-ui/public/js/views/templates.js create mode 100644 web-ui/public/locales/en_US/translation.json create mode 100644 web-ui/public/locales/pt_BR/translation.json create mode 100644 web-ui/public/locales/sv_SE/translation.json create mode 100644 web-ui/public/robots.txt create mode 100644 web-ui/public/sandbox.html create mode 100644 web-ui/public/scss/_mixins.scss create mode 100644 web-ui/public/scss/_others.scss create mode 100644 web-ui/public/scss/base/_colors.scss create mode 100644 web-ui/public/scss/base/_fonts.scss create mode 100644 web-ui/public/scss/base/_scaffolding.scss create mode 100644 web-ui/public/scss/mixins/_position-helpers.scss create mode 100644 web-ui/public/scss/mixins/_tags.scss create mode 100644 web-ui/public/scss/sandbox.scss create mode 100644 web-ui/public/scss/style.scss create mode 100644 web-ui/public/scss/templates/_no-content-placeholder.scss create mode 100644 web-ui/public/scss/templates/_unread-count.scss create mode 100644 web-ui/public/scss/vendor/_customfont.scss create mode 100644 web-ui/public/scss/vendor/_foundation.scss create mode 100644 web-ui/public/scss/vendor/_reset.scss create mode 100644 web-ui/public/scss/vendor/_scut.scss create mode 100644 web-ui/public/scss/views/_action-bar.scss create mode 100644 web-ui/public/scss/views/_close-button.scss create mode 100644 web-ui/public/scss/views/_compose-button.scss create mode 100644 web-ui/public/scss/views/_compose-view.scss create mode 100644 web-ui/public/scss/views/_mail-list.scss create mode 100644 web-ui/public/scss/views/_message-panel.scss create mode 100644 web-ui/public/scss/views/_navigation.scss create mode 100644 web-ui/public/scss/views/_no-mails-available.scss create mode 100644 web-ui/public/scss/views/_no-message-selected.scss create mode 100644 web-ui/public/scss/views/_read-view.scss create mode 100644 web-ui/public/scss/views/_security-labels.scss create mode 100644 web-ui/public/templates/compose/attachment_item.hbs create mode 100644 web-ui/public/templates/compose/attachment_upload_item.hbs create mode 100644 web-ui/public/templates/compose/attachments_list.hbs create mode 100644 web-ui/public/templates/compose/compose_box.hbs create mode 100644 web-ui/public/templates/compose/feedback_box.hbs create mode 100644 web-ui/public/templates/compose/fixed_recipient.hbs create mode 100644 web-ui/public/templates/compose/inline_box.hbs create mode 100644 web-ui/public/templates/compose/no_mails_available.hbs create mode 100644 web-ui/public/templates/compose/no_message_selected.hbs create mode 100644 web-ui/public/templates/compose/recipient_input.hbs create mode 100644 web-ui/public/templates/compose/recipients.hbs create mode 100644 web-ui/public/templates/compose/reply_section.hbs create mode 100644 web-ui/public/templates/compose/upload_attachment_failed.hbs create mode 100644 web-ui/public/templates/feedback/feedback_trigger.hbs create mode 100644 web-ui/public/templates/mail_actions/actions_box.hbs create mode 100644 web-ui/public/templates/mail_actions/compose_trigger.hbs create mode 100644 web-ui/public/templates/mail_actions/pagination_trigger.hbs create mode 100644 web-ui/public/templates/mail_actions/refresh_trigger.hbs create mode 100644 web-ui/public/templates/mail_actions/trash_actions_box.hbs create mode 100644 web-ui/public/templates/mails/draft.hbs create mode 100644 web-ui/public/templates/mails/full_view.hbs create mode 100644 web-ui/public/templates/mails/mail_actions.hbs create mode 100644 web-ui/public/templates/mails/sent.hbs create mode 100644 web-ui/public/templates/mails/single.hbs create mode 100644 web-ui/public/templates/mails/trash.hbs create mode 100644 web-ui/public/templates/page/logout.hbs create mode 100644 web-ui/public/templates/page/logout_shortcut.hbs create mode 100644 web-ui/public/templates/page/user_settings_box.hbs create mode 100644 web-ui/public/templates/page/user_settings_icon.hbs create mode 100644 web-ui/public/templates/page/version.hbs create mode 100644 web-ui/public/templates/search/search_trigger.hbs create mode 100644 web-ui/public/templates/tags/shortcut.hbs create mode 100644 web-ui/public/templates/tags/tag.hbs create mode 100644 web-ui/public/templates/tags/tag_inner.hbs create mode 100644 web-ui/public/templates/tags/tag_list.hbs create mode 100644 web-ui/public/templates/user_alerts/message.hbs diff --git a/.gitignore b/.gitignore index ddaadaa5..67c044d9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.DS_Store *.egg-info /web-ui/node_modules -/web-ui/app/bower_components +/web-ui/public/bower_components /web-ui/lib/ /web-ui/public/signup.js .tmp @@ -13,8 +13,8 @@ dist/ *archive.zip *.swp *.swo -/web-ui/app/js/generated -/web-ui/app/css +/web-ui/public/js/generated +/web-ui/public/css test-results.xml /control_tower.html /state.yml diff --git a/doc/first-steps.md b/doc/first-steps.md index e4a24097..0485b236 100644 --- a/doc/first-steps.md +++ b/doc/first-steps.md @@ -55,7 +55,7 @@ After that take some minutes to familiarize yourself with the user interface. To get a better feeling for the code base, let's try some smaller changes. Let's assume that we'd like to change the way subjects are displayed in the mail list. First we want to find the location in the code that renders the subjects. -Start your favorite text editor and open pixelated-user-agent/web-ui/app/js/mail_list/ui/mail_items/mail_item.js. Find the method named render. This seems to be the right location. To verify our assumption, let's change the html content. +Start your favorite text editor and open pixelated-user-agent/web-ui/public/js/mail_list/ui/mail_items/mail_item.js. Find the method named render. This seems to be the right location. To verify our assumption, let's change the html content. ```javascript this.render = function () { @@ -139,7 +139,7 @@ cd web-ui Now refresh your browser again to see the changes in effect. Finally we would like to change the color of the highlighting. The pixleated user agent uses [SASS](http://sass-lang.com/) to make handling styles a little bit easier. -You can find the style sheets in web-ui/app/scss/. The search-hightlight is defined in styles.scss: +You can find the style sheets in web-ui/public/scss/. The search-hightlight is defined in styles.scss: ```scss .search-highlight { diff --git a/service/pixelated/application.py b/service/pixelated/application.py index 9f33cb82..0c2383dd 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -88,12 +88,22 @@ def _create_service_factory(args): return ServicesFactory(UserAgentMode(is_single_user=False)) +def get_templates_folder(): + return os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets") + + +def get_static_folder(): + # TODO: make sure sandbox keeps working + # TODO: make sure this works for packaging + return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "web-ui", "public")) + + def initialize(): log.info('Starting the Pixelated user agent') args = arguments.parse_user_agent_args() logger.init(debug=args.debug) services_factory = _create_service_factory(args) - resource = RootResource(services_factory) + resource = RootResource(services_factory, templates_folder=get_templates_folder(), static_folder=get_static_folder()) def start(): start_async = _start_mode(args, resource, services_factory) @@ -154,7 +164,7 @@ def _setup_multi_user(args, root_resource, services_factory): def set_up_protected_resources(root_resource, provider, services_factory, banner=None, authenticator=None): session_checker = SessionChecker(services_factory) - anonymous_resource = RootResource(services_factory, public=True) + anonymous_resource = RootResource(services_factory, templates_folder=get_templates_folder(), static_folder=get_static_folder(), public=True) realm = PixelatedRealm(root_resource, anonymous_resource) _portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()]) diff --git a/service/pixelated/resources/inbox_resource.py b/service/pixelated/resources/inbox_resource.py index 426de5cc..d9ba86a5 100644 --- a/service/pixelated/resources/inbox_resource.py +++ b/service/pixelated/resources/inbox_resource.py @@ -46,7 +46,7 @@ class InboxResource(BaseResource): def _get_not_quite_the_templates_folder(self): path = os.path.dirname(os.path.abspath(pixelated.__file__)) - return os.path.join(path, '..', '..', 'web-ui', 'app') + return os.path.join(path, '..', '..', 'web-ui', 'public') def _get_templates_folder(self): path = os.path.dirname(os.path.abspath(pixelated.__file__)) diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index 7d61ddce..6300efdb 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -51,11 +51,11 @@ def _get_public_folder(): def _get_static_folder(): - static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "public")) # this is a workaround for packaging if not os.path.exists(static_folder): static_folder = os.path.abspath( - os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "public")) if not os.path.exists(static_folder): static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') return static_folder diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 1d32935b..375f27f6 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -44,13 +44,13 @@ logger = Logger() class RootResource(BaseResource): - def __init__(self, services_factory, public=False): + def __init__(self, services_factory, templates_folder, static_folder, public=False): BaseResource.__init__(self, services_factory) self._public = public self._assets_folder = self._get_assets_folder() self._startup_assets_folder = self._get_startup_folder() - self._static_folder = self._get_static_folder() - self._html_template = open(os.path.join(self._static_folder, 'index.html')).read() + self._static_folder = static_folder + self._html_template = open(os.path.join(templates_folder, 'index.html')).read() self._services_factory = services_factory with open(os.path.join(self._startup_assets_folder, 'Interstitial.html')) as f: self.interstitial = f.read() @@ -61,6 +61,7 @@ class RootResource(BaseResource): def _startup_mode(self): self.putChildProtected('assets', File(self._assets_folder)) self.putChildPublic('startup-assets', File(self._startup_assets_folder)) + self.putChildPublic('static', File(self._static_folder)) self._mode = MODE_STARTUP logger.debug('Root in STARTUP mode. %s' % self) @@ -128,13 +129,3 @@ class RootResource(BaseResource): def _get_startup_folder(self): path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(path, '..', 'assets') - - def _get_static_folder(self): - static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) - # this is a workaround for packaging - if not os.path.exists(static_folder): - static_folder = os.path.abspath( - os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) - if not os.path.exists(static_folder): - static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') - return static_folder diff --git a/service/test/integration/test_static_files.py b/service/test/integration/test_static_files.py new file mode 100644 index 00000000..e3fa8af5 --- /dev/null +++ b/service/test/integration/test_static_files.py @@ -0,0 +1,27 @@ +# +# 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 . + + +from twisted.internet.defer import inlineCallbacks +from test.support.integration import SoledadTestBase + + +class StaticFilesTest(SoledadTestBase): + + @inlineCallbacks + def test_should_find_static_file(self): + _, request = yield self.app_test_client.get('/static/js/main.js', as_json=False, ajax=False) + self.assertEqual(200, request.responseCode) diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index 4e3758c5..c611fbd0 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -38,7 +38,7 @@ from leap.soledad.client import Soledad from leap.bitmask.mail.adaptors.soledad import SoledadMailAdaptor from pixelated.adapter.mailstore.leap_attachment_store import LeapAttachmentStore from pixelated.adapter.services.feedback_service import FeedbackService -from pixelated.application import UserAgentMode, set_up_protected_resources +from pixelated.application import UserAgentMode, set_up_protected_resources, get_static_folder, get_templates_folder from pixelated.config.sessions import LeapSession from pixelated.config.services import Services, ServicesFactory, SingleUserServicesFactory from pixelated.config.site import PixelatedSite @@ -218,10 +218,11 @@ class AppTestClient(object): services = self._test_account.services self.service_factory.add_session('someuserid', services) - self.resource = RootResource(self.service_factory) + self.resource = RootResource(self.service_factory, get_templates_folder(), get_static_folder()) provider = mock() self.resource.initialize(provider) else: + # TODO: write test for me (= self.service_factory = StubServicesFactory(self.accounts, mode) provider = mock() bonafide_checker = StubAuthenticator(provider) diff --git a/service/test/support/integration/multi_user_client.py b/service/test/support/integration/multi_user_client.py index 4b9b2864..d7ab77a0 100644 --- a/service/test/support/integration/multi_user_client.py +++ b/service/test/support/integration/multi_user_client.py @@ -19,7 +19,7 @@ from mockito import mock, when, any as ANY from pixelated.authentication import Authenticator, Authentication from twisted.internet import defer -from pixelated.application import UserAgentMode, set_up_protected_resources +from pixelated.application import UserAgentMode, set_up_protected_resources, get_static_folder, get_templates_folder from pixelated.config.services import ServicesFactory from pixelated.config.sessions import LeapSessionFactory @@ -46,7 +46,7 @@ class MultiUserClient(AppTestClient): self.service_factory = ServicesFactory(UserAgentMode(is_single_user=False)) - root_resource = RootResource(self.service_factory) + root_resource = RootResource(self.service_factory, get_templates_folder(), get_static_folder()) leap_provider = mock() self.credentials_checker = StubSRPChecker(leap_provider) self.resource = set_up_protected_resources(root_resource, leap_provider, self.service_factory) diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py index f4012b1b..7112ed96 100644 --- a/service/test/unit/resources/test_auth.py +++ b/service/test/unit/resources/test_auth.py @@ -1,4 +1,6 @@ from mockito import mock, when, any as ANY + +from pixelated.application import get_templates_folder, get_static_folder from pixelated.resources.auth import SessionChecker, PixelatedRealm, PixelatedAuthSessionWrapper from pixelated.resources.login_resource import LoginResource from pixelated.resources.root_resource import RootResource @@ -40,8 +42,8 @@ class TestPixelatedAuthSessionWrapper(unittest.TestCase): session_checker = SessionChecker(services_factory) self.portal = Portal(self.realm_mock, [session_checker, AllowAnonymousAccess()]) self.user_uuid_mock = mock() - self.root_resource = RootResource(services_factory) - self.anonymous_resource = RootResource(services_factory, public=True) + self.root_resource = RootResource(services_factory, get_templates_folder(), get_static_folder()) + self.anonymous_resource = RootResource(services_factory, get_templates_folder(), get_static_folder(), public=True) self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.root_resource, self.anonymous_resource) self.request = DummyRequest([]) diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 9d738a83..e72efe59 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -5,7 +5,7 @@ from mock import MagicMock, patch from mockito import mock, when, any as ANY import pixelated -from pixelated.application import UserAgentMode +from pixelated.application import UserAgentMode, get_templates_folder, get_static_folder from pixelated.resources import IPixelatedSession, UnAuthorizedResource from pixelated.resources.features_resource import FeaturesResource from pixelated.resources.login_resource import LoginResource @@ -22,7 +22,7 @@ from pixelated.resources.root_resource import InboxResource, RootResource, MODE_ class TestPublicRootResource(unittest.TestCase): def setUp(self): - self.public_root_resource = RootResource(mock(), public=True) + self.public_root_resource = RootResource(mock(), get_templates_folder(), get_static_folder(), public=True) self.web = DummySite(self.public_root_resource) @patch('pixelated.resources.mails_resource.events.register') @@ -105,7 +105,7 @@ class TestRootResource(unittest.TestCase): when(self.services_factory).services(ANY()).thenReturn(self.services) self.mail_service.account_email = self.MAIL_ADDRESS - self.root_resource = RootResource(self.services_factory) + self.root_resource = RootResource(self.services_factory, get_templates_folder(), get_static_folder()) self.web = DummySite(self.root_resource) @patch('pixelated.resources.mails_resource.events.register') diff --git a/web-ui/.bowerrc b/web-ui/.bowerrc index 5773025b..d1c592ed 100644 --- a/web-ui/.bowerrc +++ b/web-ui/.bowerrc @@ -1,3 +1,3 @@ { - "directory": "app/bower_components" + "directory": "public/bower_components" } diff --git a/web-ui/.jshintignore b/web-ui/.jshintignore index 6a32b1a4..8dfe4354 100644 --- a/web-ui/.jshintignore +++ b/web-ui/.jshintignore @@ -1,4 +1,4 @@ -app/node_modules -app/bower_components -app/js/lib -app/js/generated +public/node_modules +public/bower_components +public/js/lib +public/js/generated diff --git a/web-ui/.tx/config b/web-ui/.tx/config index 25299ced..04fc6bc2 100644 --- a/web-ui/.tx/config +++ b/web-ui/.tx/config @@ -2,7 +2,7 @@ host = https://www.transifex.com [pixelated-user-agent.web-ui] -file_filter = app/locales//translation.json -source_file = app/locales/en_US/translation.json +file_filter = public/locales//translation.json +source_file = public/locales/en_US/translation.json source_lang = en_US type = KEYVALUEJSON diff --git a/web-ui/app/404.html b/web-ui/app/404.html deleted file mode 100644 index fdace4ab..00000000 --- a/web-ui/app/404.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - Page Not Found :( - - - -
-

Not found :(

-

Sorry, but the page you were trying to view does not exist.

-

It looks like this was the result of either:

-
    -
  • a mistyped address
  • -
  • an out-of-date link
  • -
- - -
- - diff --git a/web-ui/app/favicon.ico b/web-ui/app/favicon.ico deleted file mode 100644 index e69de29b..00000000 diff --git a/web-ui/app/fonts/OpenSans-Bold.woff b/web-ui/app/fonts/OpenSans-Bold.woff deleted file mode 100644 index dacf3c9c..00000000 Binary files a/web-ui/app/fonts/OpenSans-Bold.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-BoldItalic.woff b/web-ui/app/fonts/OpenSans-BoldItalic.woff deleted file mode 100644 index a4e29c0f..00000000 Binary files a/web-ui/app/fonts/OpenSans-BoldItalic.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-Extrabold.woff b/web-ui/app/fonts/OpenSans-Extrabold.woff deleted file mode 100644 index 7a2e352b..00000000 Binary files a/web-ui/app/fonts/OpenSans-Extrabold.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff b/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff deleted file mode 100644 index ce3ab2e7..00000000 Binary files a/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-Italic.woff b/web-ui/app/fonts/OpenSans-Italic.woff deleted file mode 100644 index c5f6bac1..00000000 Binary files a/web-ui/app/fonts/OpenSans-Italic.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-Light.woff b/web-ui/app/fonts/OpenSans-Light.woff deleted file mode 100644 index eb601d70..00000000 Binary files a/web-ui/app/fonts/OpenSans-Light.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-Semibold.woff b/web-ui/app/fonts/OpenSans-Semibold.woff deleted file mode 100644 index 56c44944..00000000 Binary files a/web-ui/app/fonts/OpenSans-Semibold.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans-SemiboldItalic.woff b/web-ui/app/fonts/OpenSans-SemiboldItalic.woff deleted file mode 100644 index 3a439fc3..00000000 Binary files a/web-ui/app/fonts/OpenSans-SemiboldItalic.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSans.woff b/web-ui/app/fonts/OpenSans.woff deleted file mode 100644 index 77706fa6..00000000 Binary files a/web-ui/app/fonts/OpenSans.woff and /dev/null differ diff --git a/web-ui/app/fonts/OpenSansLight-Italic.woff b/web-ui/app/fonts/OpenSansLight-Italic.woff deleted file mode 100644 index 3f9f088f..00000000 Binary files a/web-ui/app/fonts/OpenSansLight-Italic.woff and /dev/null differ diff --git a/web-ui/app/fonts/icomoon.ttf b/web-ui/app/fonts/icomoon.ttf deleted file mode 100644 index 61315d04..00000000 Binary files a/web-ui/app/fonts/icomoon.ttf and /dev/null differ diff --git a/web-ui/app/fonts/icomoon.woff b/web-ui/app/fonts/icomoon.woff deleted file mode 100644 index 82f11748..00000000 Binary files a/web-ui/app/fonts/icomoon.woff and /dev/null differ diff --git a/web-ui/app/images/LOADING-transparent.gif b/web-ui/app/images/LOADING-transparent.gif deleted file mode 100644 index ac9abcde..00000000 Binary files a/web-ui/app/images/LOADING-transparent.gif and /dev/null differ diff --git a/web-ui/app/images/fa-sent.svg b/web-ui/app/images/fa-sent.svg deleted file mode 100644 index a4b4bea4..00000000 --- a/web-ui/app/images/fa-sent.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - fa-sent - Created with Sketch. - - - - - - - - - - \ No newline at end of file diff --git a/web-ui/app/images/favicon.png b/web-ui/app/images/favicon.png deleted file mode 100644 index e14841c7..00000000 Binary files a/web-ui/app/images/favicon.png and /dev/null differ diff --git a/web-ui/app/images/logo.svg b/web-ui/app/images/logo.svg deleted file mode 100644 index 6c2d8989..00000000 --- a/web-ui/app/images/logo.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web-ui/app/images/pixelated-symbol-blue-transparent-01.png b/web-ui/app/images/pixelated-symbol-blue-transparent-01.png deleted file mode 100644 index 96b92155..00000000 Binary files a/web-ui/app/images/pixelated-symbol-blue-transparent-01.png and /dev/null differ diff --git a/web-ui/app/index.html b/web-ui/app/index.html deleted file mode 100644 index 4b6a81a0..00000000 --- a/web-ui/app/index.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - -$account_email - Pixelated Mail - - - - - - - - - - -
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
    -
    - -
    -
      -
    -
    -
    -
    - -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - diff --git a/web-ui/app/js/dispatchers/left_pane_dispatcher.js b/web-ui/app/js/dispatchers/left_pane_dispatcher.js deleted file mode 100644 index 0037a88f..00000000 --- a/web-ui/app/js/dispatchers/left_pane_dispatcher.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 12222aec..00000000 --- a/web-ui/app/js/dispatchers/middle_pane_dispatcher.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 . - */ -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 = $('
    ', {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 deleted file mode 100644 index 870bcd92..00000000 --- a/web-ui/app/js/dispatchers/right_pane_dispatcher.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 . - */ - -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 = $('
    ', { 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 deleted file mode 100644 index f71d56ea..00000000 --- a/web-ui/app/js/features/features.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index a5d92266..00000000 --- a/web-ui/app/js/feedback/feedback_cache.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 598f9060..00000000 --- a/web-ui/app/js/feedback/feedback_trigger.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 42405dfe..00000000 --- a/web-ui/app/js/foundation/initialize_foundation.js +++ /dev/null @@ -1,5 +0,0 @@ - -(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 deleted file mode 100644 index 66334470..00000000 --- a/web-ui/app/js/foundation/off_canvas.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index dacf2263..00000000 --- a/web-ui/app/js/helpers/browser.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index a1e5361a..00000000 --- a/web-ui/app/js/helpers/contenttype.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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 . - */ - -/* jshint curly: false */ -define([], function () { - 'use strict'; - var exports = {}; - - // Licence: PUBLIC DOMAIN - // Author: Austin Wright - - 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=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=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 deleted file mode 100644 index 236c7a40..00000000 --- a/web-ui/app/js/helpers/iterator.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index bbf85c45..00000000 --- a/web-ui/app/js/helpers/monitored_ajax.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 443e8602..00000000 --- a/web-ui/app/js/helpers/sanitizer.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 . - */ - -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: '<em class="search-highlight">', - post: '' - }, { - // highlight tag close - pre: '</em>', - post: '' - }]; - - /** - * 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 (
    ) - */ - sanitizer.addLineBreaks = function (textPlainBody) { - return textPlainBody.replace(/(\r)?\n/g, '
    ').replace(/( )? /g, '
    '); - }; - - /** - * 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 deleted file mode 100644 index d26d9fc6..00000000 --- a/web-ui/app/js/helpers/triggering.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index ed9e0559..00000000 --- a/web-ui/app/js/helpers/view_helper.js +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 17caaa23..00000000 --- a/web-ui/app/js/lib/highlightRegex.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 deleted file mode 100644 index 1ec575da..00000000 --- a/web-ui/app/js/lib/html4-defs.js +++ /dev/null @@ -1,640 +0,0 @@ -// 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 deleted file mode 100644 index 38c9cde5..00000000 --- a/web-ui/app/js/mail_list/domain/refresher.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 7205d35c..00000000 --- a/web-ui/app/js/mail_list/ui/mail_item_factory.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 . - */ - -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 = $('
  • ', { 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 deleted file mode 100644 index 57fbafd5..00000000 --- a/web-ui/app/js/mail_list/ui/mail_items/draft_item.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 939f7e1b..00000000 --- a/web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index be664289..00000000 --- a/web-ui/app/js/mail_list/ui/mail_items/mail_item.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 9e511068..00000000 --- a/web-ui/app/js/mail_list/ui/mail_items/sent_item.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index af4821a8..00000000 --- a/web-ui/app/js/mail_list/ui/mail_list.js +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index b148cdce..00000000 --- a/web-ui/app/js/mail_list_actions/ui/archive_many_trigger.js +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index ec79cb26..00000000 --- a/web-ui/app/js/mail_list_actions/ui/compose_trigger.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index dd2f67a5..00000000 --- a/web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 69e5fde4..00000000 --- a/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 2584e453..00000000 --- a/web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index c16a2229..00000000 --- a/web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 3bc13d40..00000000 --- a/web-ui/app/js/mail_list_actions/ui/pagination_trigger.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index e0a32094..00000000 --- a/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index a16270d2..00000000 --- a/web-ui/app/js/mail_list_actions/ui/refresh_trigger.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 71c65346..00000000 --- a/web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 2232dbe4..00000000 --- a/web-ui/app/js/mail_view/data/feedback_sender.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 7a478dd8..00000000 --- a/web-ui/app/js/mail_view/data/mail_builder.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 8bb01f70..00000000 --- a/web-ui/app/js/mail_view/data/mail_sender.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index e04fc02a..00000000 --- a/web-ui/app/js/mail_view/ui/attachment_icon.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 . - */ - -define( - [ - 'flight/lib/component', - 'page/events', - 'features' - ], - - function (defineComponent, events, features) { - 'use strict'; - - return defineComponent(function () { - this.render = function () { - this.$node.html(''); - }; - - 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 deleted file mode 100644 index 4ef64960..00000000 --- a/web-ui/app/js/mail_view/ui/attachment_list.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 101dc939..00000000 --- a/web-ui/app/js/mail_view/ui/compose_box.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index afe31914..00000000 --- a/web-ui/app/js/mail_view/ui/draft_box.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 47751d91..00000000 --- a/web-ui/app/js/mail_view/ui/draft_save_status.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 4e00ece8..00000000 --- a/web-ui/app/js/mail_view/ui/feedback_box.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index a34bd55d..00000000 --- a/web-ui/app/js/mail_view/ui/forward_box.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 65cd0aaa..00000000 --- a/web-ui/app/js/mail_view/ui/mail_actions.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 3408c8af..00000000 --- a/web-ui/app/js/mail_view/ui/mail_view.js +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index c62c6b30..00000000 --- a/web-ui/app/js/mail_view/ui/no_mails_available_pane.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index a5fc2393..00000000 --- a/web-ui/app/js/mail_view/ui/no_message_selected_pane.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index c13a52b1..00000000 --- a/web-ui/app/js/mail_view/ui/recipients/recipient.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 2caa8d14..00000000 --- a/web-ui/app/js/mail_view/ui/recipients/recipients.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 8a9c4eaf..00000000 --- a/web-ui/app/js/mail_view/ui/recipients/recipients_input.js +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 624ac4f5..00000000 --- a/web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index a174d185..00000000 --- a/web-ui/app/js/mail_view/ui/reply_box.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index cbe64205..00000000 --- a/web-ui/app/js/mail_view/ui/reply_section.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 66fe1233..00000000 --- a/web-ui/app/js/mail_view/ui/send_button.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index b8836a6b..00000000 --- a/web-ui/app/js/main.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index c75fda45..00000000 --- a/web-ui/app/js/mixins/with_auto_refresh.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index b8266f28..00000000 --- a/web-ui/app/js/mixins/with_compose_inline.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 5b28a67b..00000000 --- a/web-ui/app/js/mixins/with_enable_disable_on_event.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 195b08bc..00000000 --- a/web-ui/app/js/mixins/with_feature_toggle.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index c8902f61..00000000 --- a/web-ui/app/js/mixins/with_hide_and_show.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index a088080e..00000000 --- a/web-ui/app/js/mixins/with_mail_edit_base.js +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 1a51840d..00000000 --- a/web-ui/app/js/mixins/with_mail_sandbox.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 1fc1c3bd..00000000 --- a/web-ui/app/js/mixins/with_mail_tagging.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 2c29c9a1..00000000 --- a/web-ui/app/js/monkey_patching/all.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index d0ccc4b8..00000000 --- a/web-ui/app/js/monkey_patching/array.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 . - */ -(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 deleted file mode 100644 index 363ce581..00000000 --- a/web-ui/app/js/monkey_patching/post_message.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 . - */ -/* - * 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 deleted file mode 100644 index ecaedfd8..00000000 --- a/web-ui/app/js/page/default.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 68a6aad1..00000000 --- a/web-ui/app/js/page/events.js +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 81b57db2..00000000 --- a/web-ui/app/js/page/logout.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 10a69c7d..00000000 --- a/web-ui/app/js/page/logout_shortcut.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 9bb435c4..00000000 --- a/web-ui/app/js/page/pane_contract_expand.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index ad17f3be..00000000 --- a/web-ui/app/js/page/pix_logo.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index ce0d7d04..00000000 --- a/web-ui/app/js/page/router.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 4fa11c6d..00000000 --- a/web-ui/app/js/page/router/url_params.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 89dcd47d..00000000 --- a/web-ui/app/js/page/unread_count_title.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 . - */ - - -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 deleted file mode 100644 index 9fd5e629..00000000 --- a/web-ui/app/js/page/version.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 33b16ea4..00000000 --- a/web-ui/app/js/sandbox.js +++ /dev/null @@ -1,11 +0,0 @@ -(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 deleted file mode 100644 index 831be0cd..00000000 --- a/web-ui/app/js/search/results_highlighter.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 . - */ - -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, '$1'); - }); - 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 deleted file mode 100644 index 2aff027c..00000000 --- a/web-ui/app/js/search/search_trigger.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 0dfc1bdb..00000000 --- a/web-ui/app/js/services/delete_service.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 5e4bd4f3..00000000 --- a/web-ui/app/js/services/mail_service.js +++ /dev/null @@ -1,335 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 64a10c1c..00000000 --- a/web-ui/app/js/services/model/mail.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index d7d9cdc9..00000000 --- a/web-ui/app/js/services/recover_service.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 32c213cf..00000000 --- a/web-ui/app/js/style_guide/main.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 . - */ -$(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 deleted file mode 100644 index 31703b2a..00000000 --- a/web-ui/app/js/tags/data/tags.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 37814cfc..00000000 --- a/web-ui/app/js/tags/ui/tag.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 9dc1ccbb..00000000 --- a/web-ui/app/js/tags/ui/tag_base.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index a2172c6d..00000000 --- a/web-ui/app/js/tags/ui/tag_list.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index e944a7a5..00000000 --- a/web-ui/app/js/user_alerts/ui/user_alerts.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index dac29cec..00000000 --- a/web-ui/app/js/user_settings/data/user_settings.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index d3de23ed..00000000 --- a/web-ui/app/js/user_settings/ui/user_settings_box.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index a6385dc1..00000000 --- a/web-ui/app/js/user_settings/ui/user_settings_icon.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 . - */ -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 = $('
    '); - $(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 deleted file mode 100644 index 29a1beca..00000000 --- a/web-ui/app/js/views/i18n.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 0b887142..00000000 --- a/web-ui/app/js/views/recipientListFormatter.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 . - */ - -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('', '')); - var cc = _.map(header.cc, wrapWith('cc: ', '')); - var bcc = _.map(header.bcc, wrapWith('bcc: ', '')); - - 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 deleted file mode 100644 index d4185471..00000000 --- a/web-ui/app/js/views/templates.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 . - */ - -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 deleted file mode 100644 index 3e006156..00000000 --- a/web-ui/app/locales/en_US/translation.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "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-recipient": "", - "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 deleted file mode 100644 index ff766a2b..00000000 --- a/web-ui/app/locales/pt_BR/translation.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "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": "", - "no-recipient": "", - "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 deleted file mode 100644 index d4da0711..00000000 --- a/web-ui/app/locales/sv_SE/translation.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "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": "", - "no-recipient": "", - "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 deleted file mode 100644 index 6b0157e2..00000000 --- a/web-ui/app/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# robotstxt.org - -User-agent: * \ No newline at end of file diff --git a/web-ui/app/sandbox.html b/web-ui/app/sandbox.html deleted file mode 100644 index 8325b0da..00000000 --- a/web-ui/app/sandbox.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/web-ui/app/scss/_mixins.scss b/web-ui/app/scss/_mixins.scss deleted file mode 100644 index d3aa0220..00000000 --- a/web-ui/app/scss/_mixins.scss +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index 039d94bd..00000000 --- a/web-ui/app/scss/_others.scss +++ /dev/null @@ -1,72 +0,0 @@ -.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 deleted file mode 100644 index 17333ff9..00000000 --- a/web-ui/app/scss/base/_colors.scss +++ /dev/null @@ -1,64 +0,0 @@ -/* 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 deleted file mode 100644 index dfc56dd8..00000000 --- a/web-ui/app/scss/base/_fonts.scss +++ /dev/null @@ -1,68 +0,0 @@ -@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 deleted file mode 100644 index b8b5fa3b..00000000 --- a/web-ui/app/scss/base/_scaffolding.scss +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 254bfc6c..00000000 --- a/web-ui/app/scss/mixins/_position-helpers.scss +++ /dev/null @@ -1,9 +0,0 @@ -@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 deleted file mode 100644 index 9bb287ea..00000000 --- a/web-ui/app/scss/mixins/_tags.scss +++ /dev/null @@ -1,110 +0,0 @@ -$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 deleted file mode 100644 index 3c1be358..00000000 --- a/web-ui/app/scss/sandbox.scss +++ /dev/null @@ -1,27 +0,0 @@ -$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 deleted file mode 100644 index e99ab194..00000000 --- a/web-ui/app/scss/style.scss +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index c6807011..00000000 --- a/web-ui/app/scss/templates/_no-content-placeholder.scss +++ /dev/null @@ -1,5 +0,0 @@ -.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 deleted file mode 100644 index f7852227..00000000 --- a/web-ui/app/scss/templates/_unread-count.scss +++ /dev/null @@ -1,14 +0,0 @@ -.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 deleted file mode 100644 index d72cca0f..00000000 --- a/web-ui/app/scss/vendor/_customfont.scss +++ /dev/null @@ -1,9 +0,0 @@ -[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 deleted file mode 100644 index 7918cf26..00000000 --- a/web-ui/app/scss/vendor/_foundation.scss +++ /dev/null @@ -1,2066 +0,0 @@ -@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 deleted file mode 100644 index 55f8d054..00000000 --- a/web-ui/app/scss/vendor/_reset.scss +++ /dev/null @@ -1,421 +0,0 @@ -/*! 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 deleted file mode 100644 index 3e16fa65..00000000 --- a/web-ui/app/scss/vendor/_scut.scss +++ /dev/null @@ -1,1518 +0,0 @@ -/* -* 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 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
  • 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 deleted file mode 100644 index 40e677b0..00000000 --- a/web-ui/app/scss/views/_action-bar.scss +++ /dev/null @@ -1,159 +0,0 @@ -#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 deleted file mode 100644 index 37171c18..00000000 --- a/web-ui/app/scss/views/_close-button.scss +++ /dev/null @@ -1,22 +0,0 @@ -.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 deleted file mode 100644 index 81e0bb33..00000000 --- a/web-ui/app/scss/views/_compose-button.scss +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 9e120357..00000000 --- a/web-ui/app/scss/views/_compose-view.scss +++ /dev/null @@ -1,451 +0,0 @@ -.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 deleted file mode 100644 index f5c4c60f..00000000 --- a/web-ui/app/scss/views/_mail-list.scss +++ /dev/null @@ -1,124 +0,0 @@ -.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 deleted file mode 100644 index 4a0a7a6b..00000000 --- a/web-ui/app/scss/views/_message-panel.scss +++ /dev/null @@ -1,26 +0,0 @@ -.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 deleted file mode 100644 index 2c33a791..00000000 --- a/web-ui/app/scss/views/_navigation.scss +++ /dev/null @@ -1,589 +0,0 @@ -#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 deleted file mode 100644 index bf5d256a..00000000 --- a/web-ui/app/scss/views/_no-mails-available.scss +++ /dev/null @@ -1,3 +0,0 @@ -.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 deleted file mode 100644 index 0e367bf2..00000000 --- a/web-ui/app/scss/views/_no-message-selected.scss +++ /dev/null @@ -1,14 +0,0 @@ -.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 deleted file mode 100644 index f69d51a5..00000000 --- a/web-ui/app/scss/views/_read-view.scss +++ /dev/null @@ -1,165 +0,0 @@ -.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 deleted file mode 100644 index ac966ded..00000000 --- a/web-ui/app/scss/views/_security-labels.scss +++ /dev/null @@ -1,67 +0,0 @@ -.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 deleted file mode 100644 index 7a64f6f5..00000000 --- a/web-ui/app/templates/compose/attachment_item.hbs +++ /dev/null @@ -1,4 +0,0 @@ -
  • - {{ this.name }} ({{ formatSize this.size}}) - {{#if removable}}{{/if}} -
  • diff --git a/web-ui/app/templates/compose/attachment_upload_item.hbs b/web-ui/app/templates/compose/attachment_upload_item.hbs deleted file mode 100644 index eb6c4ba6..00000000 --- a/web-ui/app/templates/compose/attachment_upload_item.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
  • -
    - {{ this.name }} ({{ formatSize this.size}}) - -
  • diff --git a/web-ui/app/templates/compose/attachments_list.hbs b/web-ui/app/templates/compose/attachments_list.hbs deleted file mode 100644 index 6f34df9e..00000000 --- a/web-ui/app/templates/compose/attachments_list.hbs +++ /dev/null @@ -1,14 +0,0 @@ -
    - - - -
    -
      - {{#each attachments }} - {{> attachment_item this }} - {{/each }} -
    -
      -
      - -
      diff --git a/web-ui/app/templates/compose/compose_box.hbs b/web-ui/app/templates/compose/compose_box.hbs deleted file mode 100644 index fcfbeaaf..00000000 --- a/web-ui/app/templates/compose/compose_box.hbs +++ /dev/null @@ -1,32 +0,0 @@ - - -
      - - {{> recipients }} - - -
      - - -
      -
      - - -
      - - {{> attachments_list }} - -
      - -
      - -
      -
      - -
      diff --git a/web-ui/app/templates/compose/feedback_box.hbs b/web-ui/app/templates/compose/feedback_box.hbs deleted file mode 100644 index 346a6192..00000000 --- a/web-ui/app/templates/compose/feedback_box.hbs +++ /dev/null @@ -1,18 +0,0 @@ - - -
      -
      - Feedback -
      - -
      - - -
      - -
      - -
      -
      diff --git a/web-ui/app/templates/compose/fixed_recipient.hbs b/web-ui/app/templates/compose/fixed_recipient.hbs deleted file mode 100644 index 8b01717c..00000000 --- a/web-ui/app/templates/compose/fixed_recipient.hbs +++ /dev/null @@ -1,8 +0,0 @@ -
      - -
      - {{ address }} -
      -
      - -
      diff --git a/web-ui/app/templates/compose/inline_box.hbs b/web-ui/app/templates/compose/inline_box.hbs deleted file mode 100644 index c9c114ec..00000000 --- a/web-ui/app/templates/compose/inline_box.hbs +++ /dev/null @@ -1,20 +0,0 @@ -
      -

      {{subject}}

      - -
      - - -
      - {{t 'to'}}: {{formatRecipients recipients}} - - -{{> recipients }} - -{{> attachments_list }} - -
      - -
      - -
      -
      diff --git a/web-ui/app/templates/compose/no_mails_available.hbs b/web-ui/app/templates/compose/no_mails_available.hbs deleted file mode 100644 index c61152a4..00000000 --- a/web-ui/app/templates/compose/no_mails_available.hbs +++ /dev/null @@ -1,7 +0,0 @@ -
      - {{#if forSearch }} - {{t 'no-results-for'}}: '{{ forSearch }}'. - {{else}} - {{t 'no-emails-in'}} '{{t tag}}'. - {{/if}} -
      diff --git a/web-ui/app/templates/compose/no_message_selected.hbs b/web-ui/app/templates/compose/no_message_selected.hbs deleted file mode 100644 index 0b9beaf8..00000000 --- a/web-ui/app/templates/compose/no_message_selected.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
      -
      {{t 'nothing-selected'}}.
      -
      diff --git a/web-ui/app/templates/compose/recipient_input.hbs b/web-ui/app/templates/compose/recipient_input.hbs deleted file mode 100644 index 9416f11f..00000000 --- a/web-ui/app/templates/compose/recipient_input.hbs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web-ui/app/templates/compose/recipients.hbs b/web-ui/app/templates/compose/recipients.hbs deleted file mode 100644 index 43aced1c..00000000 --- a/web-ui/app/templates/compose/recipients.hbs +++ /dev/null @@ -1,33 +0,0 @@ - diff --git a/web-ui/app/templates/compose/reply_section.hbs b/web-ui/app/templates/compose/reply_section.hbs deleted file mode 100644 index 45203d87..00000000 --- a/web-ui/app/templates/compose/reply_section.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
      - - - - -
      diff --git a/web-ui/app/templates/compose/upload_attachment_failed.hbs b/web-ui/app/templates/compose/upload_attachment_failed.hbs deleted file mode 100644 index dbb1437b..00000000 --- a/web-ui/app/templates/compose/upload_attachment_failed.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
      - - Upload failed. This file exceeds the 1MB limit. - Choose another file - Dismiss -
      diff --git a/web-ui/app/templates/feedback/feedback_trigger.hbs b/web-ui/app/templates/feedback/feedback_trigger.hbs deleted file mode 100644 index 7f3f8ef1..00000000 --- a/web-ui/app/templates/feedback/feedback_trigger.hbs +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/web-ui/app/templates/mail_actions/actions_box.hbs b/web-ui/app/templates/mail_actions/actions_box.hbs deleted file mode 100644 index 68a8d0bf..00000000 --- a/web-ui/app/templates/mail_actions/actions_box.hbs +++ /dev/null @@ -1,7 +0,0 @@ -
    • -
    • -
    • -
    • -
    • -
    • -
    • diff --git a/web-ui/app/templates/mail_actions/compose_trigger.hbs b/web-ui/app/templates/mail_actions/compose_trigger.hbs deleted file mode 100644 index 06f05fca..00000000 --- a/web-ui/app/templates/mail_actions/compose_trigger.hbs +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/web-ui/app/templates/mail_actions/pagination_trigger.hbs b/web-ui/app/templates/mail_actions/pagination_trigger.hbs deleted file mode 100644 index cbd8a089..00000000 --- a/web-ui/app/templates/mail_actions/pagination_trigger.hbs +++ /dev/null @@ -1,3 +0,0 @@ - -{{ currentPage }} - diff --git a/web-ui/app/templates/mail_actions/refresh_trigger.hbs b/web-ui/app/templates/mail_actions/refresh_trigger.hbs deleted file mode 100644 index dffc7090..00000000 --- a/web-ui/app/templates/mail_actions/refresh_trigger.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
      - -
      diff --git a/web-ui/app/templates/mail_actions/trash_actions_box.hbs b/web-ui/app/templates/mail_actions/trash_actions_box.hbs deleted file mode 100644 index 4e0ec332..00000000 --- a/web-ui/app/templates/mail_actions/trash_actions_box.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
    • -
    • -
    • -
    • -
    • diff --git a/web-ui/app/templates/mails/draft.hbs b/web-ui/app/templates/mails/draft.hbs deleted file mode 100644 index 808ce3ff..00000000 --- a/web-ui/app/templates/mails/draft.hbs +++ /dev/null @@ -1,41 +0,0 @@ -
      - -
      - - -
      -
      - {{t 'to'}}: - {{#if header.to }} - {{ header.to }} - {{else}} - {{t 'no-recipient'}} - {{/if}} -
      - - {{ formatDate header.date }} -
      -
      -
      - - {{#if header.subject }} - {{header.subject}} - {{else}} - {{t 'no-subject'}} - {{/if}} -
      - - {{#if attachments}} -
      - {{/if}} -
      -
        - {{#each tagsForListView }} -
      • {{ this }}
      • - {{/each }} -
      -
      - - - - diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs deleted file mode 100644 index 40bfd4a2..00000000 --- a/web-ui/app/templates/mails/full_view.hbs +++ /dev/null @@ -1,83 +0,0 @@ - - -
      -
      - - -
      - {{#if signatureStatus}} - - {{t signatureStatus.label }} - - {{/if}} - {{#if encryptionStatus}} - - {{t encryptionStatus.label }} - - {{/if}} -
      - -
      - - {{#if header.from }} - {{ header.from }} - {{else}} - {{t 'you'}} - {{/if}} - - - {{{formatRecipients header}}} -
      - -
      - {{ formatDate header.date }} -
      - -
      - -
      -

      {{ header.subject }}

      -
      - - - -
        -
      • - -
      • - - {{#each tags }} -
      • {{ this }}
      • - {{/each }} - -
      • - -
      • - -
      • - -
      • -
      -
      - - - - {{#if attachments}} -
      - -
      -

      {{ attachments.length }} attachment(s):

      - -
      - {{/if}} -
      diff --git a/web-ui/app/templates/mails/mail_actions.hbs b/web-ui/app/templates/mails/mail_actions.hbs deleted file mode 100644 index 0adfe853..00000000 --- a/web-ui/app/templates/mails/mail_actions.hbs +++ /dev/null @@ -1,6 +0,0 @@ - - -
        -
      • {{t 'reply-to-all'}}
      • -
      • {{t 'delete-this-message'}}
      • -
      diff --git a/web-ui/app/templates/mails/sent.hbs b/web-ui/app/templates/mails/sent.hbs deleted file mode 100644 index 158b20c8..00000000 --- a/web-ui/app/templates/mails/sent.hbs +++ /dev/null @@ -1,36 +0,0 @@ -
      - -
      - -
      -
      - {{t 'to'}}: - {{#if header.to }} - {{ header.to }} - {{else}} - {{t 'no-recipient'}} - {{/if}} -
      - - {{ formatDate header.date }} -
      -
      -
      - {{#if header.subject }} - {{header.subject}} - {{else}} - {{t 'no-subject'}} - {{/if}} -
      - - {{#if attachments}} -
      - {{/if}} -
      -
        - {{#each tagsForListView }} -
      • {{ this }}
      • - {{/each }} -
      -
      - diff --git a/web-ui/app/templates/mails/single.hbs b/web-ui/app/templates/mails/single.hbs deleted file mode 100644 index aaede844..00000000 --- a/web-ui/app/templates/mails/single.hbs +++ /dev/null @@ -1,28 +0,0 @@ -
      - -
      - -
      -
      - {{#if header.from }} - {{ header.from }} - {{else}} - {{t "you"}} - {{/if}} -
      - - {{ formatDate header.date }} -
      -
      -
      {{ header.subject }}
      - - {{#if attachments}} -
      - {{/if}} -
      -
        - {{#each tagsForListView }} -
      • {{ this }}
      • - {{/each }} -
      -
      diff --git a/web-ui/app/templates/mails/trash.hbs b/web-ui/app/templates/mails/trash.hbs deleted file mode 100644 index f8947b15..00000000 --- a/web-ui/app/templates/mails/trash.hbs +++ /dev/null @@ -1,32 +0,0 @@ -
      - -
      - -
      -
      - {{#if header.from }} - {{ header.from }} - {{else}} - {{t "you"}} - {{/if}} -
      - - {{ formatDate header.date }} -
      -
      -
      - - {{ header.subject }} -
      - - {{#if attachments}} -
      - {{/if}} -
      -
        - {{#each tagsForListView }} -
      • {{ this }}
      • - {{/each }} -
      -
      - diff --git a/web-ui/app/templates/page/logout.hbs b/web-ui/app/templates/page/logout.hbs deleted file mode 100644 index 0cc079bc..00000000 --- a/web-ui/app/templates/page/logout.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
        - - -
      • -
        - {{t 'logout'}} -
      • - -
      diff --git a/web-ui/app/templates/page/logout_shortcut.hbs b/web-ui/app/templates/page/logout_shortcut.hbs deleted file mode 100644 index 043ab0dc..00000000 --- a/web-ui/app/templates/page/logout_shortcut.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
    • - - -
      {{t 'logout'}}
      -
      -
    • diff --git a/web-ui/app/templates/page/user_settings_box.hbs b/web-ui/app/templates/page/user_settings_box.hbs deleted file mode 100644 index 2152b779..00000000 --- a/web-ui/app/templates/page/user_settings_box.hbs +++ /dev/null @@ -1,10 +0,0 @@ -
      - - -

      {{t 'user-account'}}

      - -
      -

      {{t 'email-address'}}

      -

      {{ account_email }}

      -

      {{t 'public-key-fingerprint'}}

      -

      {{ formatFingerPrint fingerprint }}

      diff --git a/web-ui/app/templates/page/user_settings_icon.hbs b/web-ui/app/templates/page/user_settings_icon.hbs deleted file mode 100644 index 8f2f9215..00000000 --- a/web-ui/app/templates/page/user_settings_icon.hbs +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/web-ui/app/templates/page/version.hbs b/web-ui/app/templates/page/version.hbs deleted file mode 100644 index 5f43f78a..00000000 --- a/web-ui/app/templates/page/version.hbs +++ /dev/null @@ -1,2 +0,0 @@ -{{t 'version'}}: UNKNOWN_VERSION
      - diff --git a/web-ui/app/templates/search/search_trigger.hbs b/web-ui/app/templates/search/search_trigger.hbs deleted file mode 100644 index 2261d154..00000000 --- a/web-ui/app/templates/search/search_trigger.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
      - -
      diff --git a/web-ui/app/templates/tags/shortcut.hbs b/web-ui/app/templates/tags/shortcut.hbs deleted file mode 100644 index 1e82d6a9..00000000 --- a/web-ui/app/templates/tags/shortcut.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
    • - - {{#if displayBadge }} - {{ count }} - {{/if}} - -
      {{ tagName }}
      -
      -
    • diff --git a/web-ui/app/templates/tags/tag.hbs b/web-ui/app/templates/tags/tag.hbs deleted file mode 100644 index ca397b9a..00000000 --- a/web-ui/app/templates/tags/tag.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
    • - {{> tag_inner }} -
    • diff --git a/web-ui/app/templates/tags/tag_inner.hbs b/web-ui/app/templates/tags/tag_inner.hbs deleted file mode 100644 index 2e0958cb..00000000 --- a/web-ui/app/templates/tags/tag_inner.hbs +++ /dev/null @@ -1,4 +0,0 @@ -{{ tagName }} -{{#if displayBadge }} -{{ count }} -{{/if}} diff --git a/web-ui/app/templates/tags/tag_list.hbs b/web-ui/app/templates/tags/tag_list.hbs deleted file mode 100644 index 92a73283..00000000 --- a/web-ui/app/templates/tags/tag_list.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
        -
        - - {{t 'tags.tags'}} -
        -
          diff --git a/web-ui/app/templates/user_alerts/message.hbs b/web-ui/app/templates/user_alerts/message.hbs deleted file mode 100644 index abba1f91..00000000 --- a/web-ui/app/templates/user_alerts/message.hbs +++ /dev/null @@ -1 +0,0 @@ -{{ message.content }} diff --git a/web-ui/config/add_git_version.sh b/web-ui/config/add_git_version.sh index a0abefc0..abda150a 100755 --- a/web-ui/config/add_git_version.sh +++ b/web-ui/config/add_git_version.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -TEMPLATE_FILE="app/js/generated/hbs/templates.js" +TEMPLATE_FILE="public/js/generated/hbs/templates.js" COMMITISH=$(git rev-parse --short HEAD) COMMITDATE=$(git show -s --format=%cd) diff --git a/web-ui/config/buildoptions.js b/web-ui/config/buildoptions.js index 63c91653..2e7ba028 100644 --- a/web-ui/config/buildoptions.js +++ b/web-ui/config/buildoptions.js @@ -16,11 +16,11 @@ */ ({ - baseUrl: '../app', + baseUrl: '../public', wrap: true, almond: true, optimize: 'none', - mainConfigFile: '../app/js/main.js', + mainConfigFile: '../public/js/main.js', out: '../.tmp/app.concatenated.js', include: ['js/main'], name: 'bower_components/almond/almond' diff --git a/web-ui/config/compass.rb b/web-ui/config/compass.rb index 9c52a04e..bc0301a3 100644 --- a/web-ui/config/compass.rb +++ b/web-ui/config/compass.rb @@ -17,14 +17,14 @@ # Set this to the root of your project when deployed: -target_dir = ENV['PIXELATED_BUILD'] == 'package' ? 'dist' : 'app' +target_dir = ENV['PIXELATED_BUILD'] == 'package' ? 'dist' : 'public' http_path = "/" css_dir = "#{target_dir}/css" -sass_dir = "app/scss" -images_dir = "app/images" -javascripts_dir = "app/js" +sass_dir = "public/scss" +images_dir = "public/images" +javascripts_dir = "public/js" # You can select your preferred output style here (can be overridden via the command line): # output_style = :expanded or :nested or :compact or :compressed diff --git a/web-ui/config/control-tower.yml b/web-ui/config/control-tower.yml index 874e1b64..2d4d5865 100644 --- a/web-ui/config/control-tower.yml +++ b/web-ui/config/control-tower.yml @@ -1,3 +1,3 @@ --- -include_pattern: 'app/js/**/*.js' +include_pattern: 'public/js/**/*.js' exclude_pattern: '' diff --git a/web-ui/config/imagemin.js b/web-ui/config/imagemin.js index 2b2c87e0..bbc589a8 100644 --- a/web-ui/config/imagemin.js +++ b/web-ui/config/imagemin.js @@ -17,7 +17,7 @@ const imagemin = require('imagemin'); -imagemin(['app/images/*.{gif,jpg,png,svg}'], +imagemin(['public/images/*.{gif,jpg,png,svg}'], 'dist/images' ).then(files => { console.log('Images list:') diff --git a/web-ui/config/package.sh b/web-ui/config/package.sh index 165eeff3..d4208ecb 100644 --- a/web-ui/config/package.sh +++ b/web-ui/config/package.sh @@ -34,7 +34,7 @@ mkdir -p dist # copy files -cd app +cd public cp --parents \ 404.html \ fonts/* \ @@ -47,19 +47,19 @@ cd - # concat js files and minify for app.min.js cat \ -app/bower_components/modernizr/modernizr.js \ -app/bower_components/lodash/dist/lodash.js \ -app/bower_components/jquery/dist/jquery.js \ -app/bower_components/jquery-ui/jquery-ui.js \ -app/bower_components/jquery-file-upload/js/jquery.fileupload.js \ -app/js/lib/highlightRegex.js \ -app/bower_components/handlebars/handlebars.js \ -app/bower_components/typeahead.js/dist/typeahead.bundle.js \ -app/bower_components/foundation/js/foundation.js \ -app/bower_components/foundation/js/foundation/foundation.reveal.js \ -app/bower_components/foundation/js/foundation/foundation.offcanvas.js \ -app/js/foundation/initialize_foundation.js \ -app/bower_components/iframe-resizer/js/iframeResizer.js \ +public/bower_components/modernizr/modernizr.js \ +public/bower_components/lodash/dist/lodash.js \ +public/bower_components/jquery/dist/jquery.js \ +public/bower_components/jquery-ui/jquery-ui.js \ +public/bower_components/jquery-file-upload/js/jquery.fileupload.js \ +public/js/lib/highlightRegex.js \ +public/bower_components/handlebars/handlebars.js \ +public/bower_components/typeahead.js/dist/typeahead.bundle.js \ +public/bower_components/foundation/js/foundation.js \ +public/bower_components/foundation/js/foundation/foundation.reveal.js \ +public/bower_components/foundation/js/foundation/foundation.offcanvas.js \ +public/js/foundation/initialize_foundation.js \ +public/bower_components/iframe-resizer/js/iframeResizer.js \ .tmp/app.concatenated.js > dist/app.js node_modules/.bin/minify dist/app.js > dist/app.min.js rm dist/app.js @@ -72,7 +72,7 @@ fi # concat js files and minify for sandbox.min.js cat \ -app/js/sandbox.js \ -app/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js > dist/sandbox.js +public/js/sandbox.js \ +public/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js > dist/sandbox.js node_modules/.bin/minify dist/sandbox.js > dist/sandbox.min.js rm dist/sandbox.js diff --git a/web-ui/karma.conf.js b/web-ui/karma.conf.js index 52b54f57..93628052 100644 --- a/web-ui/karma.conf.js +++ b/web-ui/karma.conf.js @@ -17,51 +17,51 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ // loaded without require - 'app/bower_components/lodash/dist/lodash.js', - 'app/bower_components/jquery/dist/jquery.js', - 'app/bower_components/jquery-ui/jquery-ui.min.js', - 'app/bower_components/jquery-file-upload/js/jquery.fileupload.js', - 'app/bower_components/jasmine-jquery/lib/jasmine-jquery.js', - 'app/bower_components/jasmine-flight/lib/jasmine-flight.js', - 'app/bower_components/jasmine-jquery/lib/jasmine-jquery.js', - 'app/bower_components/handlebars/handlebars.min.js', - 'app/bower_components/modernizr/modernizr.js', - 'app/bower_components/foundation/js/foundation.js', - 'app/bower_components/foundation/js/foundation/foundation.reveal.js', - 'app/bower_components/foundation/js/foundation/foundation.offcanvas.js', - 'app/js/lib/highlightRegex.js', + 'public/bower_components/lodash/dist/lodash.js', + 'public/bower_components/jquery/dist/jquery.js', + 'public/bower_components/jquery-ui/jquery-ui.min.js', + 'public/bower_components/jquery-file-upload/js/jquery.fileupload.js', + 'public/bower_components/jasmine-jquery/lib/jasmine-jquery.js', + 'public/bower_components/jasmine-flight/lib/jasmine-flight.js', + 'public/bower_components/jasmine-jquery/lib/jasmine-jquery.js', + 'public/bower_components/handlebars/handlebars.min.js', + 'public/bower_components/modernizr/modernizr.js', + 'public/bower_components/foundation/js/foundation.js', + 'public/bower_components/foundation/js/foundation/foundation.reveal.js', + 'public/bower_components/foundation/js/foundation/foundation.offcanvas.js', + 'public/js/lib/highlightRegex.js', // hack to load RequireJS after the shim libs 'node_modules/requirejs/require.js', 'node_modules/karma-requirejs/lib/adapter.js', // loaded with require - {pattern: 'app/bower_components/DOMPurify/dist/purify.min.js', included: false}, - {pattern: 'app/bower_components/he/he.js', included: false}, - {pattern: 'app/bower_components/flight/**/*.js', included: false}, - {pattern: 'app/bower_components/i18next/**/*.js', included: false}, - {pattern: 'app/bower_components/i18next-xhr-backend/**/*.js', included: false}, - {pattern: 'app/bower_components/i18next-browser-languagedetector/**/*.js', included: false}, - {pattern: 'app/bower_components/quoted-printable/*.js', included: false}, - {pattern: 'app/bower_components/utf8/utf8.js', included: false}, - {pattern: 'app/locales/**/*.json', included: false}, - {pattern: 'app/js/**/*.js', included: false}, + {pattern: 'public/bower_components/DOMPurify/dist/purify.min.js', included: false}, + {pattern: 'public/bower_components/he/he.js', included: false}, + {pattern: 'public/bower_components/flight/**/*.js', included: false}, + {pattern: 'public/bower_components/i18next/**/*.js', included: false}, + {pattern: 'public/bower_components/i18next-xhr-backend/**/*.js', included: false}, + {pattern: 'public/bower_components/i18next-browser-languagedetector/**/*.js', included: false}, + {pattern: 'public/bower_components/quoted-printable/*.js', included: false}, + {pattern: 'public/bower_components/utf8/utf8.js', included: false}, + {pattern: 'public/locales/**/*.json', included: false}, + {pattern: 'public/js/**/*.js', included: false}, {pattern: 'test/test_data.js', included: false}, {pattern: 'test/custom_matchers.js', included: false}, {pattern: 'test/features.js', included: false}, {pattern: 'test/spec/**/*.spec.js', included: false}, - {pattern: 'app/sandbox.html', included: true, served: true}, + {pattern: 'public/sandbox.html', included: true, served: true}, 'test/test-main.js' ], proxies: { - '/sandbox/sandbox.html': '/base/app/sandbox.html', + '/sandbox/sandbox.html': '/base/public/sandbox.html', }, // list of files to exclude exclude: [ - 'app/js/main.js' + 'public/js/main.js' ], // test results reporter to use @@ -69,7 +69,7 @@ module.exports = function (config) { reporters: ['dots', 'junit', 'coverage'], preprocessors: { - 'app/js/!(lib)/**/*.js': ['coverage'] + 'public/js/!(lib)/**/*.js': ['coverage'] }, // enable / disable watching file and executing tests whenever any file changes diff --git a/web-ui/package.json b/web-ui/package.json index b937502f..3fa1d294 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -31,19 +31,19 @@ "debug": "npm run build && node_modules/karma/bin/karma start --browsers Chrome $GRUNT_OPTS", "watch": "npm run compass-watch & npm run handlebars-watch", "watch-test": "node_modules/karma/bin/karma start", - "handlebars": "mkdir -p app/js/generated/hbs/ && node_modules/handlebars/bin/handlebars app/templates/**/*.hbs > app/js/generated/hbs/templates.js --namespace=window.Pixelated --root .", - "handlebars-watch": "node_modules/.bin/watch 'npm run handlebars' app/templates", + "handlebars": "mkdir -p public/js/generated/hbs/ && node_modules/handlebars/bin/handlebars public/templates/**/*.hbs > public/js/generated/hbs/templates.js --namespace=window.Pixelated --root .", + "handlebars-watch": "node_modules/.bin/watch 'npm run handlebars' public/templates", "compass": "compass compile", "compass-watch": "compass watch", "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass && npm run build-signup", "build-signup": "babel src/js -d lib/js && browserify lib/js/index.js >public/signup.js", - "jshint": "node_modules/jshint/bin/jshint --config=.jshintrc app test", - "clean": "rm -rf .tmp/ 'dist/*' app/js/generated/hbs/* app/css/*", + "jshint": "node_modules/jshint/bin/jshint --config=.jshintrc public test", + "clean": "rm -rf .tmp/ 'dist/*' public/js/generated/hbs/* public/css/*", "buildmain": "node_modules/requirejs/bin/r.js -o config/buildoptions.js", "package": "/bin/bash config/package.sh", "imagemin": "node config/imagemin.js", - "minify_html": "node_modules/.bin/html-minifier app/index.html --collapse-whitespace | sed 's|.*||' > dist/index.html", - "minify_sandbox": "node_modules/.bin/html-minifier app/sandbox.html --collapse-whitespace | sed 's|.*||' > dist/sandbox.html", + "minify_html": "node_modules/.bin/html-minifier public/index.html --collapse-whitespace | sed 's|.*||' > dist/index.html", + "minify_sandbox": "node_modules/.bin/html-minifier public/sandbox.html --collapse-whitespace | sed 's|.*||' > dist/sandbox.html", "add_git_version": "/bin/bash config/add_git_version.sh" }, "dependencies": { diff --git a/web-ui/public/404.html b/web-ui/public/404.html new file mode 100644 index 00000000..fdace4ab --- /dev/null +++ b/web-ui/public/404.html @@ -0,0 +1,157 @@ + + + + + Page Not Found :( + + + +
          +

          Not found :(

          +

          Sorry, but the page you were trying to view does not exist.

          +

          It looks like this was the result of either:

          +
            +
          • a mistyped address
          • +
          • an out-of-date link
          • +
          + + +
          + + diff --git a/web-ui/public/favicon.ico b/web-ui/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/web-ui/public/fonts/OpenSans-Bold.woff b/web-ui/public/fonts/OpenSans-Bold.woff new file mode 100644 index 00000000..dacf3c9c Binary files /dev/null and b/web-ui/public/fonts/OpenSans-Bold.woff differ diff --git a/web-ui/public/fonts/OpenSans-BoldItalic.woff b/web-ui/public/fonts/OpenSans-BoldItalic.woff new file mode 100644 index 00000000..a4e29c0f Binary files /dev/null and b/web-ui/public/fonts/OpenSans-BoldItalic.woff differ diff --git a/web-ui/public/fonts/OpenSans-Extrabold.woff b/web-ui/public/fonts/OpenSans-Extrabold.woff new file mode 100644 index 00000000..7a2e352b Binary files /dev/null and b/web-ui/public/fonts/OpenSans-Extrabold.woff differ diff --git a/web-ui/public/fonts/OpenSans-ExtraboldItalic.woff b/web-ui/public/fonts/OpenSans-ExtraboldItalic.woff new file mode 100644 index 00000000..ce3ab2e7 Binary files /dev/null and b/web-ui/public/fonts/OpenSans-ExtraboldItalic.woff differ diff --git a/web-ui/public/fonts/OpenSans-Italic.woff b/web-ui/public/fonts/OpenSans-Italic.woff new file mode 100644 index 00000000..c5f6bac1 Binary files /dev/null and b/web-ui/public/fonts/OpenSans-Italic.woff differ diff --git a/web-ui/public/fonts/OpenSans-Light.woff b/web-ui/public/fonts/OpenSans-Light.woff new file mode 100644 index 00000000..eb601d70 Binary files /dev/null and b/web-ui/public/fonts/OpenSans-Light.woff differ diff --git a/web-ui/public/fonts/OpenSans-Semibold.woff b/web-ui/public/fonts/OpenSans-Semibold.woff new file mode 100644 index 00000000..56c44944 Binary files /dev/null and b/web-ui/public/fonts/OpenSans-Semibold.woff differ diff --git a/web-ui/public/fonts/OpenSans-SemiboldItalic.woff b/web-ui/public/fonts/OpenSans-SemiboldItalic.woff new file mode 100644 index 00000000..3a439fc3 Binary files /dev/null and b/web-ui/public/fonts/OpenSans-SemiboldItalic.woff differ diff --git a/web-ui/public/fonts/OpenSans.woff b/web-ui/public/fonts/OpenSans.woff new file mode 100644 index 00000000..77706fa6 Binary files /dev/null and b/web-ui/public/fonts/OpenSans.woff differ diff --git a/web-ui/public/fonts/OpenSansLight-Italic.woff b/web-ui/public/fonts/OpenSansLight-Italic.woff new file mode 100644 index 00000000..3f9f088f Binary files /dev/null and b/web-ui/public/fonts/OpenSansLight-Italic.woff differ diff --git a/web-ui/public/fonts/icomoon.ttf b/web-ui/public/fonts/icomoon.ttf new file mode 100644 index 00000000..61315d04 Binary files /dev/null and b/web-ui/public/fonts/icomoon.ttf differ diff --git a/web-ui/public/fonts/icomoon.woff b/web-ui/public/fonts/icomoon.woff new file mode 100644 index 00000000..82f11748 Binary files /dev/null and b/web-ui/public/fonts/icomoon.woff differ diff --git a/web-ui/public/images/LOADING-transparent.gif b/web-ui/public/images/LOADING-transparent.gif new file mode 100644 index 00000000..ac9abcde Binary files /dev/null and b/web-ui/public/images/LOADING-transparent.gif differ diff --git a/web-ui/public/images/fa-sent.svg b/web-ui/public/images/fa-sent.svg new file mode 100644 index 00000000..a4b4bea4 --- /dev/null +++ b/web-ui/public/images/fa-sent.svg @@ -0,0 +1,15 @@ + + + + fa-sent + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/web-ui/public/images/favicon.png b/web-ui/public/images/favicon.png new file mode 100644 index 00000000..e14841c7 Binary files /dev/null and b/web-ui/public/images/favicon.png differ diff --git a/web-ui/public/images/logo.svg b/web-ui/public/images/logo.svg new file mode 100644 index 00000000..6c2d8989 --- /dev/null +++ b/web-ui/public/images/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/web-ui/public/images/pixelated-symbol-blue-transparent-01.png b/web-ui/public/images/pixelated-symbol-blue-transparent-01.png new file mode 100644 index 00000000..96b92155 Binary files /dev/null and b/web-ui/public/images/pixelated-symbol-blue-transparent-01.png differ diff --git a/web-ui/public/index.html b/web-ui/public/index.html new file mode 100644 index 00000000..4b6a81a0 --- /dev/null +++ b/web-ui/public/index.html @@ -0,0 +1,113 @@ + + + + + + +$account_email - Pixelated Mail + + + + + + + + + + +
          +
          +
          +
          + +
          + + +
          +
          +
          +
          +
          +
          +
          +
          +
          +
            +
            + +
            +
              +
            +
            +
            +
            + +
            +
            +
            +
            + + + + + + + + + + + + + + + + + + + + diff --git a/web-ui/public/js/dispatchers/left_pane_dispatcher.js b/web-ui/public/js/dispatchers/left_pane_dispatcher.js new file mode 100644 index 00000000..0037a88f --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/dispatchers/middle_pane_dispatcher.js b/web-ui/public/js/dispatchers/middle_pane_dispatcher.js new file mode 100644 index 00000000..12222aec --- /dev/null +++ b/web-ui/public/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 . + */ +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 = $('
            ', {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/public/js/dispatchers/right_pane_dispatcher.js b/web-ui/public/js/dispatchers/right_pane_dispatcher.js new file mode 100644 index 00000000..870bcd92 --- /dev/null +++ b/web-ui/public/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 . + */ + +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 = $('
            ', { 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/public/js/features/features.js b/web-ui/public/js/features/features.js new file mode 100644 index 00000000..f71d56ea --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/feedback/feedback_cache.js b/web-ui/public/js/feedback/feedback_cache.js new file mode 100644 index 00000000..a5d92266 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/feedback/feedback_trigger.js b/web-ui/public/js/feedback/feedback_trigger.js new file mode 100644 index 00000000..598f9060 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/foundation/initialize_foundation.js b/web-ui/public/js/foundation/initialize_foundation.js new file mode 100644 index 00000000..42405dfe --- /dev/null +++ b/web-ui/public/js/foundation/initialize_foundation.js @@ -0,0 +1,5 @@ + +(function() { + 'use strict'; + $(document).foundation(); +})(); diff --git a/web-ui/public/js/foundation/off_canvas.js b/web-ui/public/js/foundation/off_canvas.js new file mode 100644 index 00000000..66334470 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/helpers/browser.js b/web-ui/public/js/helpers/browser.js new file mode 100644 index 00000000..dacf2263 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/helpers/contenttype.js b/web-ui/public/js/helpers/contenttype.js new file mode 100644 index 00000000..a1e5361a --- /dev/null +++ b/web-ui/public/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 . + */ + +/* jshint curly: false */ +define([], function () { + 'use strict'; + var exports = {}; + + // Licence: PUBLIC DOMAIN + // Author: Austin Wright + + 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=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=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/public/js/helpers/iterator.js b/web-ui/public/js/helpers/iterator.js new file mode 100644 index 00000000..236c7a40 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/helpers/monitored_ajax.js b/web-ui/public/js/helpers/monitored_ajax.js new file mode 100644 index 00000000..bbf85c45 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/helpers/sanitizer.js b/web-ui/public/js/helpers/sanitizer.js new file mode 100644 index 00000000..443e8602 --- /dev/null +++ b/web-ui/public/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 . + */ + +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: '<em class="search-highlight">', + post: '' + }, { + // highlight tag close + pre: '</em>', + post: '' + }]; + + /** + * 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 (
            ) + */ + sanitizer.addLineBreaks = function (textPlainBody) { + return textPlainBody.replace(/(\r)?\n/g, '
            ').replace(/( )? /g, '
            '); + }; + + /** + * 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/public/js/helpers/triggering.js b/web-ui/public/js/helpers/triggering.js new file mode 100644 index 00000000..d26d9fc6 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/helpers/view_helper.js b/web-ui/public/js/helpers/view_helper.js new file mode 100644 index 00000000..ed9e0559 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/lib/highlightRegex.js b/web-ui/public/js/lib/highlightRegex.js new file mode 100644 index 00000000..17caaa23 --- /dev/null +++ b/web-ui/public/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/public/js/lib/html4-defs.js b/web-ui/public/js/lib/html4-defs.js new file mode 100644 index 00000000..1ec575da --- /dev/null +++ b/web-ui/public/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/public/js/mail_list/domain/refresher.js b/web-ui/public/js/mail_list/domain/refresher.js new file mode 100644 index 00000000..38c9cde5 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list/ui/mail_item_factory.js b/web-ui/public/js/mail_list/ui/mail_item_factory.js new file mode 100644 index 00000000..7205d35c --- /dev/null +++ b/web-ui/public/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 . + */ + +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 = $('
          • ', { 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/public/js/mail_list/ui/mail_items/draft_item.js b/web-ui/public/js/mail_list/ui/mail_items/draft_item.js new file mode 100644 index 00000000..57fbafd5 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_list/ui/mail_items/generic_mail_item.js b/web-ui/public/js/mail_list/ui/mail_items/generic_mail_item.js new file mode 100644 index 00000000..939f7e1b --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_list/ui/mail_items/mail_item.js b/web-ui/public/js/mail_list/ui/mail_items/mail_item.js new file mode 100644 index 00000000..be664289 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_list/ui/mail_items/sent_item.js b/web-ui/public/js/mail_list/ui/mail_items/sent_item.js new file mode 100644 index 00000000..9e511068 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_list/ui/mail_list.js b/web-ui/public/js/mail_list/ui/mail_list.js new file mode 100644 index 00000000..af4821a8 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_list_actions/ui/archive_many_trigger.js b/web-ui/public/js/mail_list_actions/ui/archive_many_trigger.js new file mode 100644 index 00000000..b148cdce --- /dev/null +++ b/web-ui/public/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/public/js/mail_list_actions/ui/compose_trigger.js b/web-ui/public/js/mail_list_actions/ui/compose_trigger.js new file mode 100644 index 00000000..ec79cb26 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/delete_many_trigger.js b/web-ui/public/js/mail_list_actions/ui/delete_many_trigger.js new file mode 100644 index 00000000..dd2f67a5 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/mail_list_actions.js b/web-ui/public/js/mail_list_actions/ui/mail_list_actions.js new file mode 100644 index 00000000..69e5fde4 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_list_actions/ui/mark_as_unread_trigger.js b/web-ui/public/js/mail_list_actions/ui/mark_as_unread_trigger.js new file mode 100644 index 00000000..2584e453 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js b/web-ui/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js new file mode 100644 index 00000000..c16a2229 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/pagination_trigger.js b/web-ui/public/js/mail_list_actions/ui/pagination_trigger.js new file mode 100644 index 00000000..3bc13d40 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/recover_many_trigger.js b/web-ui/public/js/mail_list_actions/ui/recover_many_trigger.js new file mode 100644 index 00000000..e0a32094 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/refresh_trigger.js b/web-ui/public/js/mail_list_actions/ui/refresh_trigger.js new file mode 100644 index 00000000..a16270d2 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_list_actions/ui/toggle_check_all_trigger.js b/web-ui/public/js/mail_list_actions/ui/toggle_check_all_trigger.js new file mode 100644 index 00000000..71c65346 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_view/data/feedback_sender.js b/web-ui/public/js/mail_view/data/feedback_sender.js new file mode 100644 index 00000000..2232dbe4 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_view/data/mail_builder.js b/web-ui/public/js/mail_view/data/mail_builder.js new file mode 100644 index 00000000..7a478dd8 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mail_view/data/mail_sender.js b/web-ui/public/js/mail_view/data/mail_sender.js new file mode 100644 index 00000000..8bb01f70 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mail_view/ui/attachment_icon.js b/web-ui/public/js/mail_view/ui/attachment_icon.js new file mode 100644 index 00000000..e04fc02a --- /dev/null +++ b/web-ui/public/js/mail_view/ui/attachment_icon.js @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'page/events', + 'features' + ], + + function (defineComponent, events, features) { + 'use strict'; + + return defineComponent(function () { + this.render = function () { + this.$node.html(''); + }; + + this.triggerUploadAttachment = function () { + this.trigger(document, events.mail.startUploadAttachment); + }; + + this.uploadInProgress = function (ev, data) { + this.attr.busy = true; + this.$node.addClass('busy'); + }; + + this.uploadFinished = function (ev, data) { + this.attr.busy = false; + this.$node.removeClass('busy'); + }; + + this.after('initialize', function () { + if (features.isEnabled('attachment')) { + this.render(); + this.on(document, events.mail.uploadingAttachment, this.uploadInProgress); + this.on(document, events.mail.uploadedAttachment, this.uploadFinished); + this.on(document, events.mail.failedUploadAttachment, this.uploadFinished); + } + this.on(this.$node, 'click', function() { + if (!this.attr.busy) { + this.triggerUploadAttachment(); + } + }); + }); + }); + }); diff --git a/web-ui/public/js/mail_view/ui/attachment_list.js b/web-ui/public/js/mail_view/ui/attachment_list.js new file mode 100644 index 00000000..4ef64960 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/attachment_list.js @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2015 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'views/templates', + 'page/events', + 'helpers/view_helper', + 'helpers/monitored_ajax' + ], + + function (templates, events, viewHelper, monitoredAjax) { + 'use strict'; + + function attachmentList() { + this.defaultAttrs({ + inputFileUpload: '#fileupload', + attachmentListItem: '#attachment-list-item', + attachmentUploadItem: '#attachment-upload-item', + attachmentUploadItemProgress: '#attachment-upload-item-progress', + attachmentUploadItemAbort: '#attachment-upload-item-abort', + attachmentBaseUrl: '/attachment', + attachments: [], + closeIcon: '#upload-error-close', + uploadError: '#upload-error', + dismissButton: '#dismiss-button', + uploadFileButton: '#upload-file-button' + }); + + var ONE_MEGABYTE = 1024*1024; + var ATTACHMENT_SIZE_LIMIT = 5*ONE_MEGABYTE; + + this.showAttachment = function (ev, data) { + this.trigger(document, events.mail.appendAttachment, data); + this.renderAttachmentListView(data); + }; + + this.addAttachment = function (event, data) { + this.attr.attachments.push(data); + }; + + this.renderAttachmentListView = function (data) { + var currentHtml = this.select('attachmentListItem').html(); + var item = this.buildAttachmentListItem(data); + this.select('attachmentListItem').append(item); + }; + + this.buildAttachmentListItem = function (attachment) { + var attachmentData = {ident: attachment.ident, + encoding: attachment.encoding, + name: attachment.name, + size: attachment.size, + removable: true}; + + var element = $(templates.compose.attachmentItem(attachmentData)); + var self = this; + element.find('i.remove-icon').bind('click', function(event) { + var element = $(this); + var ident = element.closest('li').attr('data-ident'); + self.trigger(document, events.mail.removeAttachment, {ident: ident, element: element}); + event.preventDefault(); + }); + return element; + }; + + this.performPreUploadCheck = function(e, data) { + if (data.originalFiles[0].size > ATTACHMENT_SIZE_LIMIT) { + return false; + } + + return true; + }; + + this.removeUploadError = function() { + var uploadError = this.select('uploadError'); + if (uploadError) { + uploadError.remove(); + } + }; + + this.showUploadError = function () { + var self = this; + + var html = $(templates.compose.uploadAttachmentFailed()); + html.insertAfter(self.select('attachmentListItem')); + + self.on(self.select('closeIcon'), 'click', dismissUploadFailed); + self.on(self.select('dismissButton'), 'click', dismissUploadFailed); + self.on(self.select('uploadFileButton'), 'click', uploadAnotherFile); + + function dismissUploadFailed(event) { + event.preventDefault(); + self.select('uploadError').remove(); + } + + function uploadAnotherFile(event) { + event.preventDefault(); + self.trigger(document, events.mail.startUploadAttachment); + } + }; + + this.showUploadProgressBar = function(e, data) { + var element = $(templates.compose.attachmentUploadItem({ + name: data.originalFiles[0].name, + size: data.originalFiles[0].size + })); + this.select('attachmentUploadItem').append(element); + this.select('attachmentUploadItem').show(); + }; + + this.hideUploadProgressBar = function() { + this.select('attachmentUploadItem').hide(); + this.select('attachmentUploadItem').empty(); + }; + + this.attachUploadAbort = function(e, data) { + this.on(this.select('attachmentUploadItemAbort'), 'click', function(e) { + data.abort(); + e.preventDefault(); + }); + }; + + this.detachUploadAbort = function() { + this.off(this.select('attachmentUploadItemAbort'), 'click'); + }; + + this.addJqueryFileUploadConfig = function() { + var self = this; + + self.removeUploadError(); + + this.select('inputFileUpload').fileupload({ + add: function(e, data) { + if (self.performPreUploadCheck(e, data)) { + self.showUploadProgressBar(e, data); + self.attachUploadAbort(e, data); + data.submit(); + } else { + self.showUploadError(); + } + }, + url: self.attr.attachmentBaseUrl, + dataType: 'json', + done: function (e, response) { + self.detachUploadAbort(); + self.hideUploadProgressBar(); + self.trigger(document, events.mail.uploadedAttachment, response.result); + }, + fail: function(e, data){ + self.detachUploadAbort(); + self.hideUploadProgressBar(); + self.trigger(document, events.mail.failedUploadAttachment); + }, + progressall: function (e, data) { + var progressRate = parseInt(data.loaded / data.total * 100, 10); + self.select('attachmentUploadItemProgress').css('width', progressRate + '%'); + } + }).bind('fileuploadstart', function (e) { + self.trigger(document, events.mail.uploadingAttachment); + }); + }; + + this.startUpload = function () { + this.addJqueryFileUploadConfig(); + this.select('inputFileUpload').click(); + }; + + this.removeAttachmentFromList = function(ident) { + for (var i = 0; i < this.attr.attachments.length; i++) { + if (this.attr.attachments[i].ident === ident) { + this.attr.attachments.remove(i); + break; + } + } + }; + + this.destroyAttachmentElement = function(element) { + element.closest('li').remove(); + }; + + this.removeAttachments = function(event, data) { + this.removeAttachmentFromList(data.ident); + this.destroyAttachmentElement(data.element); + }; + + this.after('initialize', function () { + this.addJqueryFileUploadConfig(); + this.on(document, events.mail.uploadedAttachment, this.showAttachment); + this.on(document, events.mail.startUploadAttachment, this.startUpload); + this.on(document, events.mail.appendAttachment, this.addAttachment); + this.on(document, events.mail.removeAttachment, this.removeAttachments); + }); + } + + return attachmentList; + }); diff --git a/web-ui/public/js/mail_view/ui/compose_box.js b/web-ui/public/js/mail_view/ui/compose_box.js new file mode 100644 index 00000000..101dc939 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/compose_box.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_mail_edit_base', + 'page/events', + 'mail_view/data/mail_builder' + ], + + function (defineComponent, templates, withMailEditBase, events, mailBuilder) { + 'use strict'; + + return defineComponent(composeBox, withMailEditBase); + + function composeBox() { + + this.defaultAttrs({ + 'closeButton': '.close-mail-button' + }); + + this.showNoMessageSelected = function() { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.buildMail = function(tag) { + return this.builtMail(tag).build(); + }; + + this.builtMail = function(tag) { + return mailBuilder.newMail(this.attr.ident) + .subject(this.select('subjectBox').val()) + .to(this.attr.recipientValues.to) + .cc(this.attr.recipientValues.cc) + .bcc(this.attr.recipientValues.bcc) + .body(this.select('bodyBox').val()) + .attachment(this.attr.attachments) + .tag(tag); + }; + + this.renderComposeBox = function() { + this.render(templates.compose.box, {}); + this.enableFloatlabel('input.floatlabel'); + this.enableFloatlabel('textarea.floatlabel'); + this.select('recipientsFields').show(); + this.on(this.select('closeButton'), 'click', this.showNoMessageSelected); + this.enableAutoSave(); + }; + + this.mailDeleted = function(event, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + } + }; + + this.discardDraft = function () { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.after('initialize', function () { + this.renderComposeBox(); + + this.select('toBox').focus(); + this.on(document, events.mail.deleted, this.mailDeleted); + this.on(document, events.mail.sent, this.showNoMessageSelected); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/draft_box.js b/web-ui/public/js/mail_view/ui/draft_box.js new file mode 100644 index 00000000..afe31914 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/draft_box.js @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_mail_edit_base', + 'page/events', + 'mail_view/data/mail_builder' + ], + + function (defineComponent, templates, withMailEditBase, events, mailBuilder) { + 'use strict'; + + return defineComponent(draftBox, withMailEditBase); + + function draftBox() { + this.defaultAttrs({ + closeMailButton: '.close-mail-button' + }); + + this.showNoMessageSelected = function() { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.buildMail = function(tag) { + return this.builtMail(tag).build(); + }; + + this.builtMail = function(tag) { + return mailBuilder.newMail(this.attr.ident) + .subject(this.select('subjectBox').val()) + .to(this.attr.recipientValues.to) + .cc(this.attr.recipientValues.cc) + .bcc(this.attr.recipientValues.bcc) + .body(this.select('bodyBox').val()) + .attachment(this.attr.attachments) + .tag(tag); + }; + + this.renderDraftBox = function(ev, data) { + var mail = data.mail; + var body = mail.textPlainBody; + this.attr.ident = mail.ident; + this.render(templates.compose.box, { + recipients: { + to: mail.header.to, + cc: mail.header.cc, + bcc: mail.header.bcc + }, + subject: mail.header.subject, + body: body, + attachments: this.convertToRemovableAttachments(mail.attachments) + }); + + var self = this; + this.$node.find('i.remove-icon').bind('click', function(event) { + var element = $(this); + var ident = element.closest('li').attr('data-ident'); + self.trigger(document, events.mail.removeAttachment, {ident: ident, element: element}); + event.preventDefault(); + }); + + this.enableFloatlabel('input.floatlabel'); + this.enableFloatlabel('textarea.floatlabel'); + this.select('recipientsFields').show(); + this.select('bodyBox').focus(); + this.select('tipMsg').hide(); + this.enableAutoSave(); + this.bindCollapse(); + this.on(this.select('closeMailButton'), 'click', this.showNoMessageSelected); + }; + + this.convertToRemovableAttachments = function(attachments) { + return attachments.map(function(attachment) { + attachment.removable = true; + return attachment; + }); + }; + + this.mailDeleted = function(event, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + } + }; + + this.after('initialize', function () { + this.on(this, events.mail.here, this.renderDraftBox); + this.on(document, events.mail.sent, this.showNoMessageSelected); + this.on(document, events.mail.deleted, this.mailDeleted); + this.trigger(document, events.mail.want, { mail: this.attr.mailIdent, caller: this }); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/draft_save_status.js b/web-ui/public/js/mail_view/ui/draft_save_status.js new file mode 100644 index 00000000..47751d91 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/draft_save_status.js @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +define( + [ + 'flight/lib/component', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, events, i18n) { + 'use strict'; + + return defineComponent(draftSaveStatus); + + function draftSaveStatus() { + this.setMessage = function(msg) { + var node = this.$node; + return function () { node.text(msg); }; + }; + + this.after('initialize', function () { + this.on(document, events.mail.saveDraft, this.setMessage(i18n.t('draft-saving'))); + this.on(document, events.mail.draftSaved, this.setMessage(i18n.t('draft-saved'))); + this.on(document, events.ui.mail.changedSinceLastSave, this.setMessage('')); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/feedback_box.js b/web-ui/public/js/mail_view/ui/feedback_box.js new file mode 100644 index 00000000..4e00ece8 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/feedback_box.js @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define(['flight/lib/component', 'views/templates', 'page/events', 'features', 'feedback/feedback_cache'], + function (defineComponent, templates, events, features, feedbackCache) { + 'use strict'; + + return defineComponent(function () { + this.defaultAttrs({ + 'closeButton': '.close-mail-button', + 'submitButton': '#send-button', + 'textBox': '#text-box', + }); + + this.render = function () { + this.$node.html(templates.compose.feedback()); + }; + + this.startCachingData = function () { + this.select('textBox').val(feedbackCache.getCache()); + this.select('textBox').on('change', this.cacheFeedbackData.bind(this)); + }; + + + this.cacheFeedbackData = function () { + feedbackCache.setCache(this.select('textBox').val()); + }; + + this.showNoMessageSelected = function () { + this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.submitFeedback = function () { + var feedback = this.select('textBox').val(); + this.trigger(document, events.feedback.submit, {feedback: feedback}); + feedbackCache.resetCache(); + }; + + this.showSuccessMessage = function () { + this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Thanks for your feedback!'}); + }; + + this.after('initialize', function () { + if (features.isEnabled('feedback')) { + this.render(); + this.startCachingData(); + this.on(document, events.feedback.submitted, this.showNoMessageSelected); + this.on(document, events.feedback.submitted, this.showSuccessMessage); + this.on(this.select('closeButton'), 'click', this.showNoMessageSelected); + this.on(this.select('submitButton'), 'click', this.submitFeedback); + } + }); + + }); + }); diff --git a/web-ui/public/js/mail_view/ui/forward_box.js b/web-ui/public/js/mail_view/ui/forward_box.js new file mode 100644 index 00000000..a34bd55d --- /dev/null +++ b/web-ui/public/js/mail_view/ui/forward_box.js @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_compose_inline', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) { + 'use strict'; + + return defineComponent(forwardBox, withHideAndShow, withComposeInline); + + function forwardBox() { + var fwd = function(v) { return i18n.t('fwd') + ': ' + v; }; + + this.fetchTargetMail = function (ev) { + this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this }); + }; + + this.setupForwardBox = function() { + var mail = this.attr.mail; + this.attr.subject = fwd(mail.header.subject); + this.attr.attachments = mail.attachments; + + this.renderInlineCompose('forward-box', { + subject: this.attr.subject, + recipients: { to: [], cc: []}, + body: viewHelper.quoteMail(mail), + attachments: this.convertToRemovableAttachments(mail.attachments) + }); + + var self = this; + this.$node.find('i.remove-icon').bind('click', function(event) { + var element = $(this); + var ident = element.closest('li').attr('data-ident'); + self.trigger(document, events.mail.removeAttachment, {ident: ident}); + event.preventDefault(); + }); + + this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput); + this.select('recipientsDisplay').hide(); + this.select('recipientsFields').show(); + }; + + this.convertToRemovableAttachments = function(attachments) { + return attachments.map(function(attachment) { + attachment.removable = true; + return attachment; + }); + }; + + this.showSubjectInput = function() { + this.select('subjectDisplay').hide(); + this.select('subjectInput').show(); + this.select('subjectInput').focus(); + }; + + this.buildMail = function(tag) { + var builder = this.builtMail(tag).subject(this.select('subjectInput').val()); + + var headersToFwd = ['bcc', 'cc', 'date', 'from', 'message_id', 'reply_to', 'sender', 'to']; + var header = this.attr.mail.header; + _.each(headersToFwd, function (h) { + if (!_.isUndefined(header[h])) { + builder.header('resent_' + h, header[h]); + } + }); + + return builder.build(); + }; + + this.after('initialize', function () { + this.setupForwardBox(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/mail_actions.js b/web-ui/public/js/mail_view/ui/mail_actions.js new file mode 100644 index 00000000..65cd0aaa --- /dev/null +++ b/web-ui/public/js/mail_view/ui/mail_actions.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events' + ], + + function (defineComponent, templates, events) { + 'use strict'; + + return defineComponent(mailActions); + + function mailActions() { + + this.defaultAttrs({ + replyButtonTop: '#reply-button-top', + viewMoreActions: '#view-more-actions', + replyAllButtonTop: '#reply-all-button-top', + deleteButtonTop: '#delete-button-top', + moreActions: '#more-actions' + }); + + + this.displayMailActions = function () { + + this.$node.html(templates.mails.mailActions()); + + this.select('moreActions').hide(); + + this.on(this.select('replyButtonTop'), 'click', function () { + this.trigger(document, events.ui.replyBox.showReply); + }.bind(this)); + + this.on(this.select('replyAllButtonTop'), 'click', function () { + this.trigger(document, events.ui.replyBox.showReplyAll); + this.select('moreActions').hide(); + }.bind(this)); + + this.on(this.select('deleteButtonTop'), 'click', function () { + this.trigger(document, events.ui.mail.delete, {mail: this.attr.mail}); + this.select('moreActions').hide(); + }.bind(this)); + + this.on(this.select('viewMoreActions'), 'click', function () { + this.select('moreActions').toggle(); + }.bind(this)); + + this.on(this.select('viewMoreActions'), 'blur', function (event) { + var replyButtonTopHover = this.select('replyAllButtonTop').is(':hover'); + var deleteButtonTopHover = this.select('deleteButtonTop').is(':hover'); + + if (replyButtonTopHover || deleteButtonTopHover) { + event.preventDefault(); + } else { + this.select('moreActions').hide(); + } + }.bind(this)); + + }; + + this.after('initialize', function () { + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.displayMailActions(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/mail_view.js b/web-ui/public/js/mail_view/ui/mail_view.js new file mode 100644 index 00000000..3408c8af --- /dev/null +++ b/web-ui/public/js/mail_view/ui/mail_view.js @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'mail_view/ui/mail_actions', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_mail_tagging', + 'mixins/with_mail_sandbox', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, templates, mailActions, viewHelpers, withHideAndShow, withMailTagging, withMailSandbox, events, i18n) { + 'use strict'; + + return defineComponent(mailView, mailActions, withHideAndShow, withMailTagging, withMailSandbox); + + function mailView() { + this.defaultAttrs({ + tags: '.mail-read-view__header-tags-tag', + newTagInput: '#new-tag-input', + newTagButton: '#new-tag-button', + addNew: '.mail-read-view__header-tags-new-button', + trashButton: '#trash-button', + archiveButton: '#archive-button', + closeMailButton: '.close-mail-button' + }); + + this.displayMail = function (event, data) { + this.attr.mail = data.mail; + + var signed, encrypted, attachments; + + data.mail.security_casing = data.mail.security_casing || {}; + signed = this.checkSigned(data.mail); + encrypted = this.checkEncrypted(data.mail); + attachments = data.mail.attachments.map(function (attachment) { + attachment.received = true; + return attachment; + }); + + if(data.mail.mailbox === 'sent') { + encrypted = undefined; + signed = undefined; + } + + this.$node.html(templates.mails.fullView({ + header: data.mail.header, + body: [], + statuses: viewHelpers.formatStatusClasses(data.mail.status), + ident: data.mail.ident, + tags: data.mail.tags, + encryptionStatus: encrypted, + signatureStatus: signed, + attachments: attachments + })); + + this.showMailOnSandbox(this.attr.mail); + + this.attachTagCompletion(this.attr.mail); + + this.select('tags').on('click', function (event) { + this.removeTag($(event.target).text()); + }.bind(this)); + + this.addTagLoseFocus(); + this.on(this.select('newTagButton'), 'click', this.showNewTagInput); + this.on(this.select('newTagInput'), 'keydown', this.handleKeyDown); + this.on(this.select('newTagInput'), 'blur', this.addTagLoseFocus); + this.on(this.select('trashButton'), 'click', this.moveToTrash); + this.on(this.select('closeMailButton'), 'click', this.openNoMessageSelectedPane); + + mailActions.attachTo('#mail-actions', data); + this.resetScroll(); + }; + + this.resetScroll = function(){ + $('#right-pane').scrollTop(0); + }; + + this.checkEncrypted = function(mail) { + if(_.isEmpty(mail.security_casing.locks)) { + return { + cssClass: 'security-status__label--not-encrypted', + label: 'not-encrypted' + }; + } + + var statusClass = ['security-status__label--encrypted']; + var statusLabel; + + var hasAnyEncryptionInfo = _.any(mail.security_casing.locks, function (lock) { + return lock.state === 'valid'; + }); + + if(hasAnyEncryptionInfo) { + statusLabel = 'encrypted'; + } else { + statusClass.push('--with-error'); + statusLabel = 'encryption-error'; + } + + return { + cssClass: statusClass.join(''), + label: statusLabel + }; + }; + + this.checkSigned = function(mail) { + var statusNotSigned = { + cssClass: 'security-status__label--not-signed', + label: 'not-signed' + }; + + if(_.isEmpty(mail.security_casing.imprints)) { + return statusNotSigned; + } + + var hasNoSignatureInformation = _.any(mail.security_casing.imprints, function (imprint) { + return imprint.state === 'no_signature_information'; + }); + + if(hasNoSignatureInformation) { + return statusNotSigned; + } + + var statusClass = ['security-status__label--signed']; + var statusLabel = ['signed']; + + if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_revoked'; })) { + statusClass.push('--revoked'); + statusLabel.push('signature-revoked'); + } + + if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_expired'; })) { + statusClass.push('--expired'); + statusLabel.push('signature-expired'); + } + + if(this.isNotTrusted(mail)) { + statusClass.push('--not-trusted'); + statusLabel.push('signature-not-trusted'); + } + + return { + cssClass: statusClass.join(''), + label: statusLabel.join(' ') + }; + }; + + this.isNotTrusted = function(mail){ + return _.any(mail.security_casing.imprints, function(imprint) { + if(_.isNull(imprint.seal)){ + return true; + } + var currentTrust = _.isUndefined(imprint.seal.trust) ? imprint.seal.validity : imprint.seal.trust; + return currentTrust === 'no_trust'; + }); + }; + + this.openNoMessageSelectedPane = function(ev, data) { + this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.handleKeyDown = function(event) { + var ENTER_KEY = 13; + var ESC_KEY = 27; + + if (event.which === ENTER_KEY){ + event.preventDefault(); + if (this.select('newTagInput').val().trim() !== '') { + this.createNewTag(); + } + } else if (event.which === ESC_KEY) { + event.preventDefault(); + this.addTagLoseFocus(); + } + }; + + this.addTagLoseFocus = function () { + this.select('newTagInput').hide(); + this.select('newTagInput').typeahead('val', ''); + this.select('addNew').show(); + }; + + this.showNewTagInput = function () { + this.select('newTagInput').show(); + this.select('newTagInput').focus(); + this.select('addNew').hide(); + }; + + this.removeTag = function (tag) { + tag = tag.toString(); + var filteredTags = _.without(this.attr.mail.tags, tag); + this.updateTags(this.attr.mail, filteredTags); + this.trigger(document, events.dispatchers.tags.refreshTagList); + }; + + this.moveToTrash = function(){ + this.trigger(document, events.ui.mail.delete, { mail: this.attr.mail }); + }; + + this.tagsUpdated = function(ev, data) { + data = data || {}; + this.attr.mail.tags = data.tags; + this.displayMail({}, { mail: this.attr.mail }); + }; + + this.mailDeleted = function(ev, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.mail.ident)) { + this.openNoMessageSelectedPane(); + } + }; + + this.fetchMailToShow = function () { + this.trigger(events.mail.want, {mail: this.attr.ident, caller: this}); + }; + + this.highlightMailContent = function (event, data) { + // we can't directly manipulate the iFrame to highlight the content + // so we need to take an indirection where we directly manipulate + // the mail content to accomodate the highlighting + this.trigger(document, events.mail.highlightMailContent, data); + }; + + this.after('initialize', function () { + this.on(this, events.mail.notFound, this.openNoMessageSelectedPane); + this.on(this, events.mail.here, this.highlightMailContent); + this.on(document, events.mail.display, this.displayMail); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.on(document, events.mail.tags.updated, this.tagsUpdated); + this.on(document, events.mail.deleted, this.mailDeleted); + this.fetchMailToShow(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/no_mails_available_pane.js b/web-ui/public/js/mail_view/ui/no_mails_available_pane.js new file mode 100644 index 00000000..c62c6b30 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/no_mails_available_pane.js @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_hide_and_show', + 'page/events' + ], + + function(defineComponent, templates, withHideAndShow, events) { + 'use strict'; + + //return defineComponent(noMailsAvailablePane, withHideAndShow); + return defineComponent(noMailsAvailablePane); + + function noMailsAvailablePane() { + this.defaultAttrs({ + tag: null, + forSearch: '' + }); + + var mailsQueryMatch = /-?in:"?[\w]+"?|tag:"[\w]+"/g; + + this.render = function() { + this.attr.tag = 'tags.' + this.attr.tag; + this.attr.forSearch = this.attr.forSearch.replace(mailsQueryMatch, '').trim(); + this.$node.html(templates.noMailsAvailable(this.attr)); + }; + + this.after('initialize', function () { + this.render(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/no_message_selected_pane.js b/web-ui/public/js/mail_view/ui/no_message_selected_pane.js new file mode 100644 index 00000000..a5fc2393 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/no_message_selected_pane.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_hide_and_show', + 'page/events' + ], + + function(defineComponent, templates, withHideAndShow, events) { + 'use strict'; + + return defineComponent(noMessageSelectedPane, withHideAndShow); + + function noMessageSelectedPane() { + this.render = function() { + this.$node.html(templates.noMessageSelected()); + }; + + this.after('initialize', function () { + this.render(); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipient.js b/web-ui/public/js/mail_view/ui/recipients/recipient.js new file mode 100644 index 00000000..c13a52b1 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipient.js @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events' + ], + + function (defineComponent, templates, events) { + 'use strict'; + + return defineComponent(recipient); + + function recipient() { + this.renderAndPrepend = function (nodeToPrependTo, recipient) { + var html = $(templates.compose.fixedRecipient(recipient)); + html.insertBefore(nodeToPrependTo.children().last()); + var component = new this.constructor(); + component.initialize(html, recipient); + component.attr.recipient = recipient; + return component; + }; + + this.recipientDelActions = function () { + this.on(this.$node.find('.recipient-del'), 'click', function (event) { + this.doSelect(); + this.trigger(events.ui.recipients.deleteRecipient, this); + event.preventDefault(); + }); + + this.on(this.$node.find('.recipient-del'), 'mouseover', function () { + this.$node.find('.recipient-value').addClass('deleting'); + this.$node.find('.recipient-del').addClass('deleteTooltip'); + }); + + this.on(this.$node.find('.recipient-del'), 'mouseout', function () { + this.$node.find('.recipient-value').removeClass('deleting'); + this.$node.find('.recipient-del').removeClass('deleteTooltip'); + }); + }; + + this.destroy = function () { + this.$node.remove(); + this.teardown(); + }; + + this.doSelect = function () { + this.$node.find('.recipient-value').addClass('selected'); + }; + + this.doUnselect = function () { + this.$node.find('.recipient-value').removeClass('selected'); + }; + + this.isSelected = function () { + return this.$node.find('.recipient-value').hasClass('selected'); + }; + + this.sinalizeInvalid = function () { + this.$node.find('.recipient-value>span').addClass('invalid-format'); + }; + + this.discoverEncryption = function () { + this.$node.addClass('discover-encryption'); + var p = $.getJSON('/keys?search=' + this.attr.address).promise(); + p.done(function () { + this.$node.find('.recipient-value').addClass('encrypted'); + this.$node.removeClass('discover-encryption'); + }.bind(this)); + p.fail(function () { + this.$node.find('.recipient-value').addClass('not-encrypted'); + this.$node.removeClass('discover-encryption'); + }.bind(this)); + }; + + this.getMailAddress = function() { + return this.$node.find('input[type=hidden]').val(); + }; + + this.triggerEditRecipient = function(event, element) { + this.trigger(this.$node.closest('.recipients-area'), events.ui.recipients.clickToEdit, this); + }; + + this.after('initialize', function () { + this.recipientDelActions(); + this.on('click', this.triggerEditRecipient); + + if (this.attr.invalidAddress){ + this.sinalizeInvalid(); + } else { + this.discoverEncryption(); + } + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients.js b/web-ui/public/js/mail_view/ui/recipients/recipients.js new file mode 100644 index 00000000..2caa8d14 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients.js @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events', + 'helpers/iterator', + 'mail_view/ui/recipients/recipients_input', + 'mail_view/ui/recipients/recipient', + 'mail_view/ui/recipients/recipients_iterator' + ], + function (defineComponent, templates, events, Iterator, RecipientsInput, Recipient, RecipientsIterator) { + 'use strict'; + + return defineComponent(recipients); + + function recipients() { + this.defaultAttrs({ + navigationHandler: '.recipients-navigation-handler', + recipientsList: '.recipients-list' + }); + + function getAddresses(recipients) { + return _.flatten(_.map(recipients, function (e) { return e.attr.address;})); + } + + function moveLeft() { this.attr.iterator.moveLeft(); } + function moveRight() { this.attr.iterator.moveRight(); } + function deleteCurrentRecipient() { + this.attr.iterator.deleteCurrent(); + this.addressesUpdated(); + } + + function editCurrentRecipient(event, recipient) { + var mailAddr = this.attr.iterator.current().getMailAddress(); + this.attr.iterator.deleteCurrent(); + this.attr.input.$node.val(mailAddr).focus(); + this.unselectAllRecipients(); + this.addressesUpdated(); + } + + this.clickToEditRecipient = function(event, recipient) { + this.attr.iterator = null; + var mailAddr = recipient.getMailAddress(); + + var position = this.getRecipientPosition(recipient); + this.attr.recipients.splice(position, 1); + recipient.destroy(); + + this.addressesUpdated(); + this.unselectAllRecipients(); + this.attr.input.$node.val(mailAddr).focus(); + }; + + this.getRecipientPosition = function(recipient) { + return recipient.$node.closest('.recipients-area').find('.fixed-recipient').index(recipient.$node); + }; + + this.unselectAllRecipients = function() { + this.$node.find('.recipient-value.selected').removeClass('selected'); + }; + + var SPECIAL_KEYS_ACTIONS = { + 8: deleteCurrentRecipient, + 46: deleteCurrentRecipient, + 32: editCurrentRecipient, + 13: editCurrentRecipient, + 37: moveLeft, + 39: moveRight + }; + + this.addRecipient = function(recipient) { + var newRecipient = Recipient.prototype.renderAndPrepend(this.$node.find(this.attr.recipientsList), recipient); + this.attr.recipients.push(newRecipient); + }; + + this.recipientEntered = function (event, recipient) { + this.addRecipient(recipient); + this.addressesUpdated(); + }; + + this.invalidRecipientEntered = function(event, recipient) { + recipient.invalidAddress = true; + this.addRecipient(recipient); + }; + + this.deleteRecipient = function (event, recipient) { + this.attr.iterator = null; + var position = this.getRecipientPosition(recipient); + + this.attr.recipients.splice(position, 1); + recipient.destroy(); + + this.addressesUpdated(); + }; + + this.deleteLastRecipient = function () { + this.attr.recipients.pop().destroy(); + this.addressesUpdated(); + }; + + this.enterNavigationMode = function () { + this.attr.iterator = new RecipientsIterator({ + elements: this.attr.recipients, + exitInput: this.attr.input.$node + }); + + this.attr.iterator.current().doSelect(); + this.attr.input.$node.blur(); + this.select('navigationHandler').focus(); + }; + + this.leaveNavigationMode = function () { + if(this.attr.iterator) { this.attr.iterator.current().unselect(); } + this.attr.iterator = null; + }; + + this.selectLastRecipient = function () { + if (this.attr.recipients.length === 0) { return; } + this.enterNavigationMode(); + }; + + this.attachInput = function () { + this.attr.input = RecipientsInput.prototype.attachAndReturn(this.$node.find('input[type=text]'), this.attr.name); + }; + + this.processSpecialKey = function (event) { + if(SPECIAL_KEYS_ACTIONS.hasOwnProperty(event.which)) { SPECIAL_KEYS_ACTIONS[event.which].apply(this); } + }; + + this.initializeAddresses = function () { + _.each(_.flatten(this.attr.addresses), function (address) { + this.addRecipient({ address: address, name: this.attr.name }); + }.bind(this)); + }; + + this.addressesUpdated = function() { + this.trigger(document, events.ui.recipients.updated, {recipientsName: this.attr.name, newRecipients: getAddresses(this.attr.recipients)}); + }; + + this.doCompleteRecipients = function () { + var address = this.attr.input.$node.val(); + if (!_.isEmpty(address)) { + var recipient = Recipient.prototype.renderAndPrepend(this.$node, { name: this.attr.name, address: address }); + this.attr.recipients.push(recipient); + this.attr.input.$node.val(''); + } + + this.trigger(document, events.ui.recipients.updated, { + recipientsName: this.attr.name, + newRecipients: getAddresses(this.attr.recipients), + skipSaveDraft: true + }); + + }; + + this.after('initialize', function () { + this.attr.recipients = []; + this.attachInput(); + this.initializeAddresses(); + + this.on(events.ui.recipients.deleteRecipient, this.deleteRecipient); + this.on(events.ui.recipients.deleteLast, this.deleteLastRecipient); + this.on(events.ui.recipients.selectLast, this.selectLastRecipient); + this.on(events.ui.recipients.entered, this.recipientEntered); + this.on(events.ui.recipients.enteredInvalid, this.invalidRecipientEntered); + this.on(events.ui.recipients.clickToEdit, this.clickToEditRecipient); + + this.on(document, events.ui.recipients.doCompleteInput, this.doCompleteRecipients); + + this.on(this.attr.input.$node, 'focus', this.leaveNavigationMode); + this.on(this.select('navigationHandler'), 'keydown', this.processSpecialKey); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + }); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_input.js b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js new file mode 100644 index 00000000..8a9c4eaf --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define([ + 'flight/lib/component', + 'page/events', + 'features' + ], + function (defineComponent, events, features) { + 'use strict'; + + function recipientsInput() { + var EXIT_KEY_CODE_MAP = { + 8: 'backspace', + 37: 'left' + }, + ENTER_ADDRESS_KEY_CODE_MAP = { + 9: 'tab', + 186: 'semicolon', + 188: 'comma', + 13: 'enter', + 27: 'esc' + }, + EVENT_FOR = { + 8: events.ui.recipients.deleteLast, + 37: events.ui.recipients.selectLast + }, + self; + + var simpleAddressMatch = /[^<\w,;]?([^\s<;,]+@[\w-]+\.[^\s>;,]+)/; + var canonicalAddressMatch = /([^,;\s][^,;@]+<[^\s;,]+@[\w-]+\.[^\s;,]+>)/; + var emailAddressMatch = new RegExp([simpleAddressMatch.source, '|', canonicalAddressMatch.source].join(''), 'g'); + + var extractContactNames = function (response) { + return _.map(response, function(a) { return { value: a }; }); + }; + + function createEmailCompleter() { + var emailCompleter = new Bloodhound({ + datumTokenizer: function (d) { + return [d.value]; + }, + queryTokenizer: function (q) { + return [q.trim()]; + }, + remote: { + url: '/contacts?q=%QUERY', + filter: extractContactNames + } + }); + emailCompleter.initialize(); + return emailCompleter; + } + + function reset(node) { + node.typeahead('val', ''); + } + + function caretIsInTheBeginningOfInput(input) { + return input.selectionStart === 0; + } + + function isExitKey(keyPressed) { + return EXIT_KEY_CODE_MAP.hasOwnProperty(keyPressed); + } + + function isEnterAddressKey(keyPressed) { + return ENTER_ADDRESS_KEY_CODE_MAP.hasOwnProperty(keyPressed); + } + + this.processSpecialKey = function (event) { + var keyPressed = event.which; + + if (isExitKey(keyPressed) && caretIsInTheBeginningOfInput(this.$node[0])) { + this.trigger(EVENT_FOR[keyPressed]); + return; + } + + if (!event.shiftKey && isEnterAddressKey(keyPressed)) { + this.tokenizeRecipient(event); + + if ((keyPressed !== 9 /* tab */)) { + event.preventDefault(); + } + } + + }; + + this.tokenizeRecipient = function (event) { + if (_.isEmpty(this.$node.val().trim())) { + return; + } + + this.recipientSelected(null, {value: this.$node.val() }); + event.preventDefault(); + }; + + this.recipientSelected = function (event, data) { + var value = (data && data.value) || this.$node.val(); + + var validAddresses = this.extractValidAddresses(value); + var invalidAddresses = this.extractInvalidAddresses(value); + + this.triggerEventForEach(validAddresses, events.ui.recipients.entered); + this.triggerEventForEach(invalidAddresses, events.ui.recipients.enteredInvalid); + + reset(this.$node); + }; + + this.triggerEventForEach = function (addresses, event) { + var that = this; + _.each(addresses, function(address) { + if (!_.isEmpty(address.trim())) { + that.trigger(that.$node, event, { name: that.attr.name, address: address.trim() }); + } + }); + }; + + this.extractValidAddresses = function(rawAddresses) { + return rawAddresses.match(emailAddressMatch); + }; + + this.extractInvalidAddresses = function(rawAddresses) { + return rawAddresses.replace(emailAddressMatch, '').split(/[,;]/); + }; + + this.init = function () { + this.$node.typeahead({ + hint: true, + highlight: true, + minLength: 1 + }, { + source: createEmailCompleter().ttAdapter(), + templates: { + suggestion: function (o) { return _.escape(o.value); } + } + }); + }; + + this.attachAndReturn = function (node, name) { + var input = new this.constructor(); + input.initialize(node, { name: name}); + return input; + }; + + this.warnSendButtonOfInputState = function () { + var toTrigger = _.isEmpty(this.$node.val()) ? events.ui.recipients.inputFieldIsEmpty : events.ui.recipients.inputFieldHasCharacters; + this.trigger(document, toTrigger, { name: this.attr.name }); + }; + + this.after('initialize', function () { + self = this; + this.init(); + this.on('typeahead:selected typeahead:autocompleted', this.recipientSelected); + this.on(this.$node, 'focusout', this.tokenizeRecipient); + this.on(this.$node, 'keydown', this.processSpecialKey); + this.on(this.$node, 'keyup', this.warnSendButtonOfInputState); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + + return defineComponent(recipientsInput); + + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js new file mode 100644 index 00000000..624ac4f5 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define(['helpers/iterator'], function (Iterator) { + 'use strict'; + + return RecipientsIterator; + + function RecipientsIterator(options) { + + this.iterator = new Iterator(options.elements, options.elements.length - 1); + this.input = options.exitInput; + + this.current = function () { + return this.iterator.current(); + }; + + this.moveLeft = function () { + if (this.iterator.hasPrevious()) { + this.iterator.current().doUnselect(); + this.iterator.previous().doSelect(); + } + }; + + this.moveRight = function () { + this.iterator.current().doUnselect(); + if (this.iterator.hasNext()) { + this.iterator.next().doSelect(); + } else { + this.input.focus(); + } + }; + + this.deleteCurrent = function () { + this.iterator.removeCurrent().destroy(); + + if (this.iterator.hasElements()) { + this.iterator.current().doSelect(); + } else { + this.input.focus(); + } + }; + } + +}); diff --git a/web-ui/public/js/mail_view/ui/reply_box.js b/web-ui/public/js/mail_view/ui/reply_box.js new file mode 100644 index 00000000..a174d185 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/reply_box.js @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ + +define( + [ + 'flight/lib/component', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_compose_inline', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) { + 'use strict'; + + return defineComponent(replyBox, withHideAndShow, withComposeInline); + + function replyBox() { + this.defaultAttrs({ + replyType: 'reply', + draftReply: false, + mail: null, + mailBeingRepliedIdent: undefined + }); + + this.getRecipients = function() { + if (this.attr.replyType === 'replyall') { + return this.attr.mail.replyToAllAddress(); + } else { + return this.attr.mail.replyToAddress(); + } + }; + + var re = function(v) { return i18n.t('re') + ': ' + v; }; + + this.setupReplyBox = function() { + var recipients, body; + + if (this.attr.draftReply){ + this.attr.ident = this.attr.mail.ident; + this.attr.mailBeingRepliedIdent = this.attr.mail.draft_reply_for; + + recipients = this.attr.mail.recipients(); + body = this.attr.mail.body; + this.attr.subject = this.attr.mail.header.subject; + } else { + this.attr.mailBeingRepliedIdent = this.attr.mail.ident; + recipients = this.getRecipients(); + body = viewHelper.quoteMail(this.attr.mail); + this.attr.subject = re(this.attr.mail.header.subject); + } + + this.attr.recipientValues.to = recipients.to; + this.attr.recipientValues.cc = recipients.cc; + + this.renderInlineCompose('reply-box', { + recipients: recipients, + subject: this.attr.subject, + body: body + }); + + this.on(this.select('recipientsDisplay'), 'click keydown', this.showRecipientFields); + this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput); + }; + + this.showRecipientFields = function(ev, data) { + if(!ev.keyCode || ev.keyCode === 13){ + this.select('recipientsDisplay').hide(); + this.select('recipientsFields').show(); + $('#recipients-to-area .tt-input').focus(); + } + }; + + this.showSubjectInput = function() { + this.select('subjectDisplay').hide(); + this.select('subjectInput').show(); + this.select('subjectInput').focus(); + }; + + this.buildMail = function(tag) { + var builder = this.builtMail(tag).subject(this.select('subjectInput').val()); + if(!_.isUndefined(this.attr.mail.header.message_id)) { + builder.header('in_reply_to', this.attr.mail.header.message_id); + } + + if(!_.isUndefined(this.attr.mail.header.list_id)) { + builder.header('list_id', this.attr.mail.header.list_id); + } + + var mail = builder.build(); + mail.setDraftReplyFor(this.attr.mailBeingRepliedIdent); + + return mail; + }; + + this.after('initialize', function () { + this.setupReplyBox(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/reply_section.js b/web-ui/public/js/mail_view/ui/reply_section.js new file mode 100644 index 00000000..cbe64205 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/reply_section.js @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mail_view/ui/reply_box', + 'mail_view/ui/forward_box', + 'mixins/with_hide_and_show', + 'mixins/with_feature_toggle', + 'page/events' + ], + + function (defineComponent, templates, ReplyBox, ForwardBox, withHideAndShow, withFeatureToggle, events) { + 'use strict'; + + return defineComponent(replySection, withHideAndShow, withFeatureToggle('replySection')); + + function replySection() { + this.defaultAttrs({ + replyButton: '#reply-button', + replyAllButton: '#reply-all-button', + forwardButton: '#forward-button', + replyBox: '#reply-box', + replyType: 'reply', + replyContainer: '.reply-container' + }); + + this.showReply = function() { + this.attr.replyType = 'reply'; + this.fetchEmailToReplyTo(); + }; + + this.showReplyAll = function() { + this.attr.replyType = 'replyall'; + this.fetchEmailToReplyTo(); + }; + + this.showForward = function() { + this.attr.replyType = 'forward'; + this.fetchEmailToReplyTo(); + }; + + this.render = function () { + this.$node.html(templates.compose.replySection); + + this.on(this.select('replyButton'), 'click', this.showReply); + this.on(this.select('replyAllButton'), 'click', this.showReplyAll); + this.on(this.select('forwardButton'), 'click', this.showForward); + }; + + this.checkForDraftReply = function() { + this.render(); + this.hideContainer(); + + this.trigger(document, events.mail.draftReply.want, {ident: this.attr.ident}); + }; + + this.fetchEmailToReplyTo = function (ev) { + this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this }); + }; + + this.showDraftReply = function(ev, data) { + this.showContainer(); + this.hideButtons(); + ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, draftReply: true }); + }; + + this.showReplyComposeBox = function (ev, data) { + this.showContainer(); + this.hideButtons(); + if(this.attr.replyType === 'forward') { + ForwardBox.attachTo(this.select('replyBox'), { mail: data.mail }); + } else { + ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, replyType: this.attr.replyType }); + } + }; + + this.hideContainer = function() { + this.select('replyContainer').hide(); + }; + + this.showContainer = function() { + this.select('replyContainer').show(); + }; + + this.hideButtons = function() { + this.select('replyButton').hide(); + this.select('replyAllButton').hide(); + this.select('forwardButton').hide(); + }; + + this.showButtons = function () { + this.showContainer(); + this.select('replyBox').empty(); + this.select('replyButton').show(); + this.select('replyAllButton').show(); + this.select('forwardButton').show(); + }; + + this.after('initialize', function () { + this.on(document, events.ui.replyBox.showReply, this.showReply); + this.on(document, events.ui.replyBox.showReplyAll, this.showReplyAll); + this.on(document, events.ui.composeBox.trashReply, this.showButtons); + this.on(this, events.mail.here, this.showReplyComposeBox); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + + this.on(document, events.ui.replyBox.showReplyContainer, this.showContainer); + this.on(document, events.mail.draftReply.here, this.showDraftReply); + + this.checkForDraftReply(); + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/send_button.js b/web-ui/public/js/mail_view/ui/send_button.js new file mode 100644 index 00000000..66fe1233 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/send_button.js @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see . + */ +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/public/js/main.js b/web-ui/public/js/main.js new file mode 100644 index 00000000..b8836a6b --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mixins/with_auto_refresh.js b/web-ui/public/js/mixins/with_auto_refresh.js new file mode 100644 index 00000000..c75fda45 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mixins/with_compose_inline.js b/web-ui/public/js/mixins/with_compose_inline.js new file mode 100644 index 00000000..b8266f28 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mixins/with_enable_disable_on_event.js b/web-ui/public/js/mixins/with_enable_disable_on_event.js new file mode 100644 index 00000000..5b28a67b --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mixins/with_feature_toggle.js b/web-ui/public/js/mixins/with_feature_toggle.js new file mode 100644 index 00000000..195b08bc --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mixins/with_hide_and_show.js b/web-ui/public/js/mixins/with_hide_and_show.js new file mode 100644 index 00000000..c8902f61 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mixins/with_mail_edit_base.js b/web-ui/public/js/mixins/with_mail_edit_base.js new file mode 100644 index 00000000..a088080e --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/mixins/with_mail_sandbox.js b/web-ui/public/js/mixins/with_mail_sandbox.js new file mode 100644 index 00000000..1a51840d --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/mixins/with_mail_tagging.js b/web-ui/public/js/mixins/with_mail_tagging.js new file mode 100644 index 00000000..1fc1c3bd --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/monkey_patching/all.js b/web-ui/public/js/monkey_patching/all.js new file mode 100644 index 00000000..2c29c9a1 --- /dev/null +++ b/web-ui/public/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 . + */ +require(['js/monkey_patching/array', 'js/monkey_patching/post_message'], function () {}); diff --git a/web-ui/public/js/monkey_patching/array.js b/web-ui/public/js/monkey_patching/array.js new file mode 100644 index 00000000..d0ccc4b8 --- /dev/null +++ b/web-ui/public/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 . + */ +(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/public/js/monkey_patching/post_message.js b/web-ui/public/js/monkey_patching/post_message.js new file mode 100644 index 00000000..363ce581 --- /dev/null +++ b/web-ui/public/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 . + */ +/* + * 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/public/js/page/default.js b/web-ui/public/js/page/default.js new file mode 100644 index 00000000..ecaedfd8 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/events.js b/web-ui/public/js/page/events.js new file mode 100644 index 00000000..68a6aad1 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/logout.js b/web-ui/public/js/page/logout.js new file mode 100644 index 00000000..81b57db2 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/logout_shortcut.js b/web-ui/public/js/page/logout_shortcut.js new file mode 100644 index 00000000..10a69c7d --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/pane_contract_expand.js b/web-ui/public/js/page/pane_contract_expand.js new file mode 100644 index 00000000..9bb435c4 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/page/pix_logo.js b/web-ui/public/js/page/pix_logo.js new file mode 100644 index 00000000..ad17f3be --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/router.js b/web-ui/public/js/page/router.js new file mode 100644 index 00000000..ce0d7d04 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/router/url_params.js b/web-ui/public/js/page/router/url_params.js new file mode 100644 index 00000000..4fa11c6d --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/page/unread_count_title.js b/web-ui/public/js/page/unread_count_title.js new file mode 100644 index 00000000..89dcd47d --- /dev/null +++ b/web-ui/public/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 . + */ + + +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/public/js/page/version.js b/web-ui/public/js/page/version.js new file mode 100644 index 00000000..9fd5e629 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/sandbox.js b/web-ui/public/js/sandbox.js new file mode 100644 index 00000000..33b16ea4 --- /dev/null +++ b/web-ui/public/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/public/js/search/results_highlighter.js b/web-ui/public/js/search/results_highlighter.js new file mode 100644 index 00000000..831be0cd --- /dev/null +++ b/web-ui/public/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 . + */ + +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, '$1'); + }); + 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/public/js/search/search_trigger.js b/web-ui/public/js/search/search_trigger.js new file mode 100644 index 00000000..2aff027c --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/services/delete_service.js b/web-ui/public/js/services/delete_service.js new file mode 100644 index 00000000..0dfc1bdb --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/services/mail_service.js b/web-ui/public/js/services/mail_service.js new file mode 100644 index 00000000..5e4bd4f3 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/services/model/mail.js b/web-ui/public/js/services/model/mail.js new file mode 100644 index 00000000..64a10c1c --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/services/recover_service.js b/web-ui/public/js/services/recover_service.js new file mode 100644 index 00000000..d7d9cdc9 --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/style_guide/main.js b/web-ui/public/js/style_guide/main.js new file mode 100644 index 00000000..32c213cf --- /dev/null +++ b/web-ui/public/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 . + */ +$(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/public/js/tags/data/tags.js b/web-ui/public/js/tags/data/tags.js new file mode 100644 index 00000000..31703b2a --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/tags/ui/tag.js b/web-ui/public/js/tags/ui/tag.js new file mode 100644 index 00000000..37814cfc --- /dev/null +++ b/web-ui/public/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 . + */ + +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/public/js/tags/ui/tag_base.js b/web-ui/public/js/tags/ui/tag_base.js new file mode 100644 index 00000000..9dc1ccbb --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/tags/ui/tag_list.js b/web-ui/public/js/tags/ui/tag_list.js new file mode 100644 index 00000000..a2172c6d --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/user_alerts/ui/user_alerts.js b/web-ui/public/js/user_alerts/ui/user_alerts.js new file mode 100644 index 00000000..e944a7a5 --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/user_settings/data/user_settings.js b/web-ui/public/js/user_settings/data/user_settings.js new file mode 100644 index 00000000..dac29cec --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/user_settings/ui/user_settings_box.js b/web-ui/public/js/user_settings/ui/user_settings_box.js new file mode 100644 index 00000000..d3de23ed --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/user_settings/ui/user_settings_icon.js b/web-ui/public/js/user_settings/ui/user_settings_icon.js new file mode 100644 index 00000000..a6385dc1 --- /dev/null +++ b/web-ui/public/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 . + */ +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 = $('
            '); + $(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/public/js/views/i18n.js b/web-ui/public/js/views/i18n.js new file mode 100644 index 00000000..29a1beca --- /dev/null +++ b/web-ui/public/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 . + */ +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/public/js/views/recipientListFormatter.js b/web-ui/public/js/views/recipientListFormatter.js new file mode 100644 index 00000000..0b887142 --- /dev/null +++ b/web-ui/public/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 . + */ + +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('', '')); + var cc = _.map(header.cc, wrapWith('cc: ', '')); + var bcc = _.map(header.bcc, wrapWith('bcc: ', '')); + + return new Handlebars.SafeString(to.concat(cc, bcc).join(', ')); + }); +}); diff --git a/web-ui/public/js/views/templates.js b/web-ui/public/js/views/templates.js new file mode 100644 index 00000000..8792f8cb --- /dev/null +++ b/web-ui/public/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 . + */ + +define(['hbs/templates'], function (templates) { + 'use strict'; + + var Templates = { + compose: { + box: window.Pixelated['public/templates/compose/compose_box.hbs'], + inlineBox: window.Pixelated['public/templates/compose/inline_box.hbs'], + replySection: window.Pixelated['public/templates/compose/reply_section.hbs'], + recipientInput: window.Pixelated['public/templates/compose/recipient_input.hbs'], + fixedRecipient: window.Pixelated['public/templates/compose/fixed_recipient.hbs'], + recipients: window.Pixelated['public/templates/compose/recipients.hbs'], + feedback: window.Pixelated['public/templates/compose/feedback_box.hbs'], + attachmentsList: window.Pixelated['public/templates/compose/attachments_list.hbs'], + attachmentItem: window.Pixelated['public/templates/compose/attachment_item.hbs'], + attachmentUploadItem: window.Pixelated['public/templates/compose/attachment_upload_item.hbs'], + uploadAttachmentFailed: window.Pixelated['public/templates/compose/upload_attachment_failed.hbs'] + }, + tags: { + tagList: window.Pixelated['public/templates/tags/tag_list.hbs'], + tag: window.Pixelated['public/templates/tags/tag.hbs'], + tagInner: window.Pixelated['public/templates/tags/tag_inner.hbs'], + shortcut: window.Pixelated['public/templates/tags/shortcut.hbs'] + }, + userAlerts: { + message: window.Pixelated['public/templates/user_alerts/message.hbs'] + }, + mails: { + single: window.Pixelated['public/templates/mails/single.hbs'], + fullView: window.Pixelated['public/templates/mails/full_view.hbs'], + mailActions: window.Pixelated['public/templates/mails/mail_actions.hbs'], + draft: window.Pixelated['public/templates/mails/draft.hbs'], + sent: window.Pixelated['public/templates/mails/sent.hbs'], + trash: window.Pixelated['public/templates/mails/trash.hbs'] + }, + mailActions: { + actionsBox: window.Pixelated['public/templates/mail_actions/actions_box.hbs'], + trashActionsBox: window.Pixelated['public/templates/mail_actions/trash_actions_box.hbs'], + composeTrigger: window.Pixelated['public/templates/mail_actions/compose_trigger.hbs'], + refreshTrigger: window.Pixelated['public/templates/mail_actions/refresh_trigger.hbs'], + paginationTrigger: window.Pixelated['public/templates/mail_actions/pagination_trigger.hbs'] + }, + noMessageSelected: window.Pixelated['public/templates/compose/no_message_selected.hbs'], + noMailsAvailable: window.Pixelated['public/templates/compose/no_mails_available.hbs'], + search: { + trigger: window.Pixelated['public/templates/search/search_trigger.hbs'] + }, + page: { + userSettingsIcon: window.Pixelated['public/templates/page/user_settings_icon.hbs'], + userSettingsBox: window.Pixelated['public/templates/page/user_settings_box.hbs'], + logout: window.Pixelated['public/templates/page/logout.hbs'], + logoutShortcut: window.Pixelated['public/templates/page/logout_shortcut.hbs'], + version: window.Pixelated['public/templates/page/version.hbs'] + }, + feedback: { + feedback: window.Pixelated['public/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/public/locales/en_US/translation.json b/web-ui/public/locales/en_US/translation.json new file mode 100644 index 00000000..3e006156 --- /dev/null +++ b/web-ui/public/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-recipient": "", + "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/public/locales/pt_BR/translation.json b/web-ui/public/locales/pt_BR/translation.json new file mode 100644 index 00000000..ff766a2b --- /dev/null +++ b/web-ui/public/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": "", + "no-recipient": "", + "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/public/locales/sv_SE/translation.json b/web-ui/public/locales/sv_SE/translation.json new file mode 100644 index 00000000..d4da0711 --- /dev/null +++ b/web-ui/public/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": "", + "no-recipient": "", + "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/public/robots.txt b/web-ui/public/robots.txt new file mode 100644 index 00000000..6b0157e2 --- /dev/null +++ b/web-ui/public/robots.txt @@ -0,0 +1,3 @@ +# robotstxt.org + +User-agent: * \ No newline at end of file diff --git a/web-ui/public/sandbox.html b/web-ui/public/sandbox.html new file mode 100644 index 00000000..8325b0da --- /dev/null +++ b/web-ui/public/sandbox.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/web-ui/public/scss/_mixins.scss b/web-ui/public/scss/_mixins.scss new file mode 100644 index 00000000..d3aa0220 --- /dev/null +++ b/web-ui/public/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/public/scss/_others.scss b/web-ui/public/scss/_others.scss new file mode 100644 index 00000000..039d94bd --- /dev/null +++ b/web-ui/public/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/public/scss/base/_colors.scss b/web-ui/public/scss/base/_colors.scss new file mode 100644 index 00000000..17333ff9 --- /dev/null +++ b/web-ui/public/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/public/scss/base/_fonts.scss b/web-ui/public/scss/base/_fonts.scss new file mode 100644 index 00000000..dfc56dd8 --- /dev/null +++ b/web-ui/public/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/public/scss/base/_scaffolding.scss b/web-ui/public/scss/base/_scaffolding.scss new file mode 100644 index 00000000..b8b5fa3b --- /dev/null +++ b/web-ui/public/scss/base/_scaffolding.scss @@ -0,0 +1,10 @@ +html { + height: 100% ; +} + +body { + min-height: 100% ; + overflow: hidden; + background: $white; +} + diff --git a/web-ui/public/scss/mixins/_position-helpers.scss b/web-ui/public/scss/mixins/_position-helpers.scss new file mode 100644 index 00000000..254bfc6c --- /dev/null +++ b/web-ui/public/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/public/scss/mixins/_tags.scss b/web-ui/public/scss/mixins/_tags.scss new file mode 100644 index 00000000..9bb287ea --- /dev/null +++ b/web-ui/public/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/public/scss/sandbox.scss b/web-ui/public/scss/sandbox.scss new file mode 100644 index 00000000..3c1be358 --- /dev/null +++ b/web-ui/public/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/public/scss/style.scss b/web-ui/public/scss/style.scss new file mode 100644 index 00000000..e99ab194 --- /dev/null +++ b/web-ui/public/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/public/scss/templates/_no-content-placeholder.scss b/web-ui/public/scss/templates/_no-content-placeholder.scss new file mode 100644 index 00000000..c6807011 --- /dev/null +++ b/web-ui/public/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/public/scss/templates/_unread-count.scss b/web-ui/public/scss/templates/_unread-count.scss new file mode 100644 index 00000000..f7852227 --- /dev/null +++ b/web-ui/public/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/public/scss/vendor/_customfont.scss b/web-ui/public/scss/vendor/_customfont.scss new file mode 100644 index 00000000..d72cca0f --- /dev/null +++ b/web-ui/public/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/public/scss/vendor/_foundation.scss b/web-ui/public/scss/vendor/_foundation.scss new file mode 100644 index 00000000..7918cf26 --- /dev/null +++ b/web-ui/public/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/public/scss/vendor/_reset.scss b/web-ui/public/scss/vendor/_reset.scss new file mode 100644 index 00000000..55f8d054 --- /dev/null +++ b/web-ui/public/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/public/scss/vendor/_scut.scss b/web-ui/public/scss/vendor/_scut.scss new file mode 100644 index 00000000..3e16fa65 --- /dev/null +++ b/web-ui/public/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 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
          • 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/public/scss/views/_action-bar.scss b/web-ui/public/scss/views/_action-bar.scss new file mode 100644 index 00000000..40e677b0 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_close-button.scss b/web-ui/public/scss/views/_close-button.scss new file mode 100644 index 00000000..37171c18 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_compose-button.scss b/web-ui/public/scss/views/_compose-button.scss new file mode 100644 index 00000000..81e0bb33 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_compose-view.scss b/web-ui/public/scss/views/_compose-view.scss new file mode 100644 index 00000000..9e120357 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_mail-list.scss b/web-ui/public/scss/views/_mail-list.scss new file mode 100644 index 00000000..f5c4c60f --- /dev/null +++ b/web-ui/public/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/public/scss/views/_message-panel.scss b/web-ui/public/scss/views/_message-panel.scss new file mode 100644 index 00000000..4a0a7a6b --- /dev/null +++ b/web-ui/public/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/public/scss/views/_navigation.scss b/web-ui/public/scss/views/_navigation.scss new file mode 100644 index 00000000..2c33a791 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_no-mails-available.scss b/web-ui/public/scss/views/_no-mails-available.scss new file mode 100644 index 00000000..bf5d256a --- /dev/null +++ b/web-ui/public/scss/views/_no-mails-available.scss @@ -0,0 +1,3 @@ +.no-mails-available-pane { + @extend .no-content-placeholder; +} diff --git a/web-ui/public/scss/views/_no-message-selected.scss b/web-ui/public/scss/views/_no-message-selected.scss new file mode 100644 index 00000000..0e367bf2 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_read-view.scss b/web-ui/public/scss/views/_read-view.scss new file mode 100644 index 00000000..f69d51a5 --- /dev/null +++ b/web-ui/public/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/public/scss/views/_security-labels.scss b/web-ui/public/scss/views/_security-labels.scss new file mode 100644 index 00000000..ac966ded --- /dev/null +++ b/web-ui/public/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/public/templates/compose/attachment_item.hbs b/web-ui/public/templates/compose/attachment_item.hbs new file mode 100644 index 00000000..7a64f6f5 --- /dev/null +++ b/web-ui/public/templates/compose/attachment_item.hbs @@ -0,0 +1,4 @@ +
          • + {{ this.name }} ({{ formatSize this.size}}) + {{#if removable}}{{/if}} +
          • diff --git a/web-ui/public/templates/compose/attachment_upload_item.hbs b/web-ui/public/templates/compose/attachment_upload_item.hbs new file mode 100644 index 00000000..eb6c4ba6 --- /dev/null +++ b/web-ui/public/templates/compose/attachment_upload_item.hbs @@ -0,0 +1,5 @@ +
          • +
            + {{ this.name }} ({{ formatSize this.size}}) + +
          • diff --git a/web-ui/public/templates/compose/attachments_list.hbs b/web-ui/public/templates/compose/attachments_list.hbs new file mode 100644 index 00000000..6f34df9e --- /dev/null +++ b/web-ui/public/templates/compose/attachments_list.hbs @@ -0,0 +1,14 @@ +
            + + + +
            +
              + {{#each attachments }} + {{> attachment_item this }} + {{/each }} +
            +
              +
              + +
              diff --git a/web-ui/public/templates/compose/compose_box.hbs b/web-ui/public/templates/compose/compose_box.hbs new file mode 100644 index 00000000..fcfbeaaf --- /dev/null +++ b/web-ui/public/templates/compose/compose_box.hbs @@ -0,0 +1,32 @@ + + +
              + + {{> recipients }} + + +
              + + +
              +
              + + +
              + + {{> attachments_list }} + +
              + +
              + +
              +
              + +
              diff --git a/web-ui/public/templates/compose/feedback_box.hbs b/web-ui/public/templates/compose/feedback_box.hbs new file mode 100644 index 00000000..346a6192 --- /dev/null +++ b/web-ui/public/templates/compose/feedback_box.hbs @@ -0,0 +1,18 @@ + + +
              +
              + Feedback +
              + +
              + + +
              + +
              + +
              +
              diff --git a/web-ui/public/templates/compose/fixed_recipient.hbs b/web-ui/public/templates/compose/fixed_recipient.hbs new file mode 100644 index 00000000..8b01717c --- /dev/null +++ b/web-ui/public/templates/compose/fixed_recipient.hbs @@ -0,0 +1,8 @@ +
              + +
              + {{ address }} +
              +
              + +
              diff --git a/web-ui/public/templates/compose/inline_box.hbs b/web-ui/public/templates/compose/inline_box.hbs new file mode 100644 index 00000000..c9c114ec --- /dev/null +++ b/web-ui/public/templates/compose/inline_box.hbs @@ -0,0 +1,20 @@ +
              +

              {{subject}}

              + +
              + + +
              + {{t 'to'}}: {{formatRecipients recipients}} + + +{{> recipients }} + +{{> attachments_list }} + +
              + +
              + +
              +
              diff --git a/web-ui/public/templates/compose/no_mails_available.hbs b/web-ui/public/templates/compose/no_mails_available.hbs new file mode 100644 index 00000000..c61152a4 --- /dev/null +++ b/web-ui/public/templates/compose/no_mails_available.hbs @@ -0,0 +1,7 @@ +
              + {{#if forSearch }} + {{t 'no-results-for'}}: '{{ forSearch }}'. + {{else}} + {{t 'no-emails-in'}} '{{t tag}}'. + {{/if}} +
              diff --git a/web-ui/public/templates/compose/no_message_selected.hbs b/web-ui/public/templates/compose/no_message_selected.hbs new file mode 100644 index 00000000..0b9beaf8 --- /dev/null +++ b/web-ui/public/templates/compose/no_message_selected.hbs @@ -0,0 +1,3 @@ +
              +
              {{t 'nothing-selected'}}.
              +
              diff --git a/web-ui/public/templates/compose/recipient_input.hbs b/web-ui/public/templates/compose/recipient_input.hbs new file mode 100644 index 00000000..9416f11f --- /dev/null +++ b/web-ui/public/templates/compose/recipient_input.hbs @@ -0,0 +1 @@ + diff --git a/web-ui/public/templates/compose/recipients.hbs b/web-ui/public/templates/compose/recipients.hbs new file mode 100644 index 00000000..43aced1c --- /dev/null +++ b/web-ui/public/templates/compose/recipients.hbs @@ -0,0 +1,33 @@ + diff --git a/web-ui/public/templates/compose/reply_section.hbs b/web-ui/public/templates/compose/reply_section.hbs new file mode 100644 index 00000000..45203d87 --- /dev/null +++ b/web-ui/public/templates/compose/reply_section.hbs @@ -0,0 +1,6 @@ +
              + + + + +
              diff --git a/web-ui/public/templates/compose/upload_attachment_failed.hbs b/web-ui/public/templates/compose/upload_attachment_failed.hbs new file mode 100644 index 00000000..dbb1437b --- /dev/null +++ b/web-ui/public/templates/compose/upload_attachment_failed.hbs @@ -0,0 +1,6 @@ +
              + + Upload failed. This file exceeds the 1MB limit. + Choose another file + Dismiss +
              diff --git a/web-ui/public/templates/feedback/feedback_trigger.hbs b/web-ui/public/templates/feedback/feedback_trigger.hbs new file mode 100644 index 00000000..7f3f8ef1 --- /dev/null +++ b/web-ui/public/templates/feedback/feedback_trigger.hbs @@ -0,0 +1,8 @@ + diff --git a/web-ui/public/templates/mail_actions/actions_box.hbs b/web-ui/public/templates/mail_actions/actions_box.hbs new file mode 100644 index 00000000..68a8d0bf --- /dev/null +++ b/web-ui/public/templates/mail_actions/actions_box.hbs @@ -0,0 +1,7 @@ +
            • +
            • +
            • +
            • +
            • +
            • +
            • diff --git a/web-ui/public/templates/mail_actions/compose_trigger.hbs b/web-ui/public/templates/mail_actions/compose_trigger.hbs new file mode 100644 index 00000000..06f05fca --- /dev/null +++ b/web-ui/public/templates/mail_actions/compose_trigger.hbs @@ -0,0 +1,3 @@ + diff --git a/web-ui/public/templates/mail_actions/pagination_trigger.hbs b/web-ui/public/templates/mail_actions/pagination_trigger.hbs new file mode 100644 index 00000000..cbd8a089 --- /dev/null +++ b/web-ui/public/templates/mail_actions/pagination_trigger.hbs @@ -0,0 +1,3 @@ + +{{ currentPage }} + diff --git a/web-ui/public/templates/mail_actions/refresh_trigger.hbs b/web-ui/public/templates/mail_actions/refresh_trigger.hbs new file mode 100644 index 00000000..dffc7090 --- /dev/null +++ b/web-ui/public/templates/mail_actions/refresh_trigger.hbs @@ -0,0 +1,3 @@ +
              + +
              diff --git a/web-ui/public/templates/mail_actions/trash_actions_box.hbs b/web-ui/public/templates/mail_actions/trash_actions_box.hbs new file mode 100644 index 00000000..4e0ec332 --- /dev/null +++ b/web-ui/public/templates/mail_actions/trash_actions_box.hbs @@ -0,0 +1,5 @@ +
            • +
            • +
            • +
            • +
            • diff --git a/web-ui/public/templates/mails/draft.hbs b/web-ui/public/templates/mails/draft.hbs new file mode 100644 index 00000000..808ce3ff --- /dev/null +++ b/web-ui/public/templates/mails/draft.hbs @@ -0,0 +1,41 @@ +
              + +
              + + +
              +
              + {{t 'to'}}: + {{#if header.to }} + {{ header.to }} + {{else}} + {{t 'no-recipient'}} + {{/if}} +
              + + {{ formatDate header.date }} +
              +
              +
              + + {{#if header.subject }} + {{header.subject}} + {{else}} + {{t 'no-subject'}} + {{/if}} +
              + + {{#if attachments}} +
              + {{/if}} +
              +
                + {{#each tagsForListView }} +
              • {{ this }}
              • + {{/each }} +
              +
              + + + + diff --git a/web-ui/public/templates/mails/full_view.hbs b/web-ui/public/templates/mails/full_view.hbs new file mode 100644 index 00000000..40bfd4a2 --- /dev/null +++ b/web-ui/public/templates/mails/full_view.hbs @@ -0,0 +1,83 @@ + + +
              +
              + + +
              + {{#if signatureStatus}} + + {{t signatureStatus.label }} + + {{/if}} + {{#if encryptionStatus}} + + {{t encryptionStatus.label }} + + {{/if}} +
              + +
              + + {{#if header.from }} + {{ header.from }} + {{else}} + {{t 'you'}} + {{/if}} + + + {{{formatRecipients header}}} +
              + +
              + {{ formatDate header.date }} +
              + +
              + +
              +

              {{ header.subject }}

              +
              + + + +
                +
              • + +
              • + + {{#each tags }} +
              • {{ this }}
              • + {{/each }} + +
              • + +
              • + +
              • + +
              • +
              +
              + + + + {{#if attachments}} +
              + +
              +

              {{ attachments.length }} attachment(s):

              + +
              + {{/if}} +
              diff --git a/web-ui/public/templates/mails/mail_actions.hbs b/web-ui/public/templates/mails/mail_actions.hbs new file mode 100644 index 00000000..0adfe853 --- /dev/null +++ b/web-ui/public/templates/mails/mail_actions.hbs @@ -0,0 +1,6 @@ + + +
                +
              • {{t 'reply-to-all'}}
              • +
              • {{t 'delete-this-message'}}
              • +
              diff --git a/web-ui/public/templates/mails/sent.hbs b/web-ui/public/templates/mails/sent.hbs new file mode 100644 index 00000000..158b20c8 --- /dev/null +++ b/web-ui/public/templates/mails/sent.hbs @@ -0,0 +1,36 @@ +
              + +
              + +
              +
              + {{t 'to'}}: + {{#if header.to }} + {{ header.to }} + {{else}} + {{t 'no-recipient'}} + {{/if}} +
              + + {{ formatDate header.date }} +
              +
              +
              + {{#if header.subject }} + {{header.subject}} + {{else}} + {{t 'no-subject'}} + {{/if}} +
              + + {{#if attachments}} +
              + {{/if}} +
              +
                + {{#each tagsForListView }} +
              • {{ this }}
              • + {{/each }} +
              +
              + diff --git a/web-ui/public/templates/mails/single.hbs b/web-ui/public/templates/mails/single.hbs new file mode 100644 index 00000000..aaede844 --- /dev/null +++ b/web-ui/public/templates/mails/single.hbs @@ -0,0 +1,28 @@ +
              + +
              + +
              +
              + {{#if header.from }} + {{ header.from }} + {{else}} + {{t "you"}} + {{/if}} +
              + + {{ formatDate header.date }} +
              +
              +
              {{ header.subject }}
              + + {{#if attachments}} +
              + {{/if}} +
              +
                + {{#each tagsForListView }} +
              • {{ this }}
              • + {{/each }} +
              +
              diff --git a/web-ui/public/templates/mails/trash.hbs b/web-ui/public/templates/mails/trash.hbs new file mode 100644 index 00000000..f8947b15 --- /dev/null +++ b/web-ui/public/templates/mails/trash.hbs @@ -0,0 +1,32 @@ +
              + +
              + +
              +
              + {{#if header.from }} + {{ header.from }} + {{else}} + {{t "you"}} + {{/if}} +
              + + {{ formatDate header.date }} +
              +
              +
              + + {{ header.subject }} +
              + + {{#if attachments}} +
              + {{/if}} +
              +
                + {{#each tagsForListView }} +
              • {{ this }}
              • + {{/each }} +
              +
              + diff --git a/web-ui/public/templates/page/logout.hbs b/web-ui/public/templates/page/logout.hbs new file mode 100644 index 00000000..0cc079bc --- /dev/null +++ b/web-ui/public/templates/page/logout.hbs @@ -0,0 +1,9 @@ +
                +
                + +
              • +
                + {{t 'logout'}} +
              • +
                +
              diff --git a/web-ui/public/templates/page/logout_shortcut.hbs b/web-ui/public/templates/page/logout_shortcut.hbs new file mode 100644 index 00000000..043ab0dc --- /dev/null +++ b/web-ui/public/templates/page/logout_shortcut.hbs @@ -0,0 +1,6 @@ +
            • + + +
              {{t 'logout'}}
              +
              +
            • diff --git a/web-ui/public/templates/page/user_settings_box.hbs b/web-ui/public/templates/page/user_settings_box.hbs new file mode 100644 index 00000000..2152b779 --- /dev/null +++ b/web-ui/public/templates/page/user_settings_box.hbs @@ -0,0 +1,10 @@ +
              + + +

              {{t 'user-account'}}

              + +
              +

              {{t 'email-address'}}

              +

              {{ account_email }}

              +

              {{t 'public-key-fingerprint'}}

              +

              {{ formatFingerPrint fingerprint }}

              diff --git a/web-ui/public/templates/page/user_settings_icon.hbs b/web-ui/public/templates/page/user_settings_icon.hbs new file mode 100644 index 00000000..8f2f9215 --- /dev/null +++ b/web-ui/public/templates/page/user_settings_icon.hbs @@ -0,0 +1,8 @@ + diff --git a/web-ui/public/templates/page/version.hbs b/web-ui/public/templates/page/version.hbs new file mode 100644 index 00000000..5f43f78a --- /dev/null +++ b/web-ui/public/templates/page/version.hbs @@ -0,0 +1,2 @@ +{{t 'version'}}: UNKNOWN_VERSION
              + diff --git a/web-ui/public/templates/search/search_trigger.hbs b/web-ui/public/templates/search/search_trigger.hbs new file mode 100644 index 00000000..2261d154 --- /dev/null +++ b/web-ui/public/templates/search/search_trigger.hbs @@ -0,0 +1,3 @@ +
              + +
              diff --git a/web-ui/public/templates/tags/shortcut.hbs b/web-ui/public/templates/tags/shortcut.hbs new file mode 100644 index 00000000..1e82d6a9 --- /dev/null +++ b/web-ui/public/templates/tags/shortcut.hbs @@ -0,0 +1,9 @@ +
            • + + {{#if displayBadge }} + {{ count }} + {{/if}} + +
              {{ tagName }}
              +
              +
            • diff --git a/web-ui/public/templates/tags/tag.hbs b/web-ui/public/templates/tags/tag.hbs new file mode 100644 index 00000000..ca397b9a --- /dev/null +++ b/web-ui/public/templates/tags/tag.hbs @@ -0,0 +1,3 @@ +
            • + {{> tag_inner }} +
            • diff --git a/web-ui/public/templates/tags/tag_inner.hbs b/web-ui/public/templates/tags/tag_inner.hbs new file mode 100644 index 00000000..2e0958cb --- /dev/null +++ b/web-ui/public/templates/tags/tag_inner.hbs @@ -0,0 +1,4 @@ +{{ tagName }} +{{#if displayBadge }} +{{ count }} +{{/if}} diff --git a/web-ui/public/templates/tags/tag_list.hbs b/web-ui/public/templates/tags/tag_list.hbs new file mode 100644 index 00000000..92a73283 --- /dev/null +++ b/web-ui/public/templates/tags/tag_list.hbs @@ -0,0 +1,6 @@ +
                +
                + + {{t 'tags.tags'}} +
                +
                  diff --git a/web-ui/public/templates/user_alerts/message.hbs b/web-ui/public/templates/user_alerts/message.hbs new file mode 100644 index 00000000..abba1f91 --- /dev/null +++ b/web-ui/public/templates/user_alerts/message.hbs @@ -0,0 +1 @@ +{{ message.content }} diff --git a/web-ui/test/test-main.js b/web-ui/test/test-main.js index 4396993f..ce76be9b 100644 --- a/web-ui/test/test-main.js +++ b/web-ui/test/test-main.js @@ -19,34 +19,34 @@ requirejs.config({ baseUrl: '/base', paths: { - 'page': 'app/js/page', - 'js': 'app/js', - 'lib': 'app/js/lib', - 'hbs': 'app/js/generated/hbs', - 'flight': 'app/bower_components/flight', - 'DOMPurify': 'app/bower_components/DOMPurify/dist/purify.min', - 'he': 'app/bower_components/he/he', - 'views': 'app/js/views', - 'helpers': 'app/js/helpers', - 'feedback': 'app/js/feedback', - 'tags': 'app/js/tags', - 'mail_list': 'app/js/mail_list', - 'mail_list_actions': 'app/js/mail_list_actions', - 'user_alerts': 'app/js/user_alerts', - 'mail_view': 'app/js/mail_view', - 'dispatchers': 'app/js/dispatchers', - 'mixins': 'app/js/mixins', - 'services': 'app/js/services', - 'search': 'app/js/search', - 'monkey_patching': 'app/js/monkey_patching', - 'i18next': 'app/bower_components/i18next/i18next', - 'i18nextXHRBackend': 'app/bower_components/i18next-xhr-backend/i18nextXHRBackend', - 'i18nextBrowserLanguageDetector': 'app/bower_components/i18next-browser-languagedetector/i18nextBrowserLanguageDetector', - 'quoted-printable': 'app/bower_components/quoted-printable', - 'utf8': 'app/bower_components/utf8', + 'page': 'public/js/page', + 'js': 'public/js', + 'lib': 'public/js/lib', + 'hbs': 'public/js/generated/hbs', + 'flight': 'public/bower_components/flight', + 'DOMPurify': 'public/bower_components/DOMPurify/dist/purify.min', + 'he': 'public/bower_components/he/he', + 'views': 'public/js/views', + 'helpers': 'public/js/helpers', + 'feedback': 'public/js/feedback', + 'tags': 'public/js/tags', + 'mail_list': 'public/js/mail_list', + 'mail_list_actions': 'public/js/mail_list_actions', + 'user_alerts': 'public/js/user_alerts', + 'mail_view': 'public/js/mail_view', + 'dispatchers': 'public/js/dispatchers', + 'mixins': 'public/js/mixins', + 'services': 'public/js/services', + 'search': 'public/js/search', + 'monkey_patching': 'public/js/monkey_patching', + 'i18next': 'public/bower_components/i18next/i18next', + 'i18nextXHRBackend': 'public/bower_components/i18next-xhr-backend/i18nextXHRBackend', + 'i18nextBrowserLanguageDetector': 'public/bower_components/i18next-browser-languagedetector/i18nextBrowserLanguageDetector', + 'quoted-printable': 'public/bower_components/quoted-printable', + 'utf8': 'public/bower_components/utf8', 'test': 'test', 'features': 'test/features', - 'user_settings': 'app/js/user_settings' + 'user_settings': 'public/js/user_settings' }, deps: tests, @@ -73,7 +73,7 @@ requirejs.config({ .init({ lng: 'en_US', backend: { - loadPath: '/base/app/locales/en_US/translation.json' + loadPath: '/base/public/locales/en_US/translation.json' } }); Handlebars.registerHelper('t', i18n.t); -- cgit v1.2.3 From a0de084e04f02a5f09d5a14b86ece156f4f6df5f Mon Sep 17 00:00:00 2001 From: Zara Gebru Date: Fri, 2 Dec 2016 17:55:02 +0100 Subject: [refactor] use static url instead of assets url --- README.md | 2 +- service/MANIFEST.in | 2 +- service/test/unit/resources/test_root_resource.py | 2 +- web-ui/.jshintignore | 1 + web-ui/package.json | 2 +- web-ui/public/index.html | 44 +++++++++++------------ web-ui/public/js/main.js | 2 +- web-ui/public/js/page/default.js | 2 +- web-ui/public/scss/base/_fonts.scss | 22 ++++++------ 9 files changed, 40 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index eb3c44c1..158ca3af 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Here's a [podcast](https://soundcloud.com/thoughtworks/pixelated-why-secure-comm **Pixelated is still in early development state!** -![High level architecture User Agent](https://raw.githubusercontent.com/pixelated/website/master/assets/images/pixelated-user-agent.png) +![High level architecture User Agent](https://raw.githubusercontent.com/pixelated/website/master/static/images/pixelated-user-agent.png) ## Try it! diff --git a/service/MANIFEST.in b/service/MANIFEST.in index a4fb90be..81d25913 100644 --- a/service/MANIFEST.in +++ b/service/MANIFEST.in @@ -1,5 +1,5 @@ include README.md recursive-include pixelated/certificates *.* -recursive-include pixelated/assets *.* +recursive-include pixelated/static *.* recursive-include debian * diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index e72efe59..d42a4b38 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -300,7 +300,7 @@ class TestRootResource(unittest.TestCase): def test_assets_should_be_publicly_available(self, *mocks): self.root_resource.initialize(provider=mock(), authenticator=mock()) - request = DummyRequest(['assets', 'dummy.json']) + request = DummyRequest(['static', 'dummy.json']) request.addCookie = MagicMock(return_value='stubbed') d = self.web.get(request) diff --git a/web-ui/.jshintignore b/web-ui/.jshintignore index 8dfe4354..473c9c23 100644 --- a/web-ui/.jshintignore +++ b/web-ui/.jshintignore @@ -2,3 +2,4 @@ public/node_modules public/bower_components public/js/lib public/js/generated +public/signup.js diff --git a/web-ui/package.json b/web-ui/package.json index 3fa1d294..b10ff51e 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -42,7 +42,7 @@ "buildmain": "node_modules/requirejs/bin/r.js -o config/buildoptions.js", "package": "/bin/bash config/package.sh", "imagemin": "node config/imagemin.js", - "minify_html": "node_modules/.bin/html-minifier public/index.html --collapse-whitespace | sed 's|.*||' > dist/index.html", + "minify_html": "node_modules/.bin/html-minifier public/index.html --collapse-whitespace | sed 's|.*||' > dist/index.html", "minify_sandbox": "node_modules/.bin/html-minifier public/sandbox.html --collapse-whitespace | sed 's|.*||' > dist/sandbox.html", "add_git_version": "/bin/bash config/add_git_version.sh" }, diff --git a/web-ui/public/index.html b/web-ui/public/index.html index 4b6a81a0..0a6303b2 100644 --- a/web-ui/public/index.html +++ b/web-ui/public/index.html @@ -1,15 +1,15 @@ - + $account_email - Pixelated Mail - - - + + + @@ -25,15 +25,15 @@ - - - - @@ -93,20 +93,20 @@
                  - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/web-ui/public/js/main.js b/web-ui/public/js/main.js index b8836a6b..1aeb1a63 100644 --- a/web-ui/public/js/main.js +++ b/web-ui/public/js/main.js @@ -16,7 +16,7 @@ */ requirejs.config({ - baseUrl: '../assets/', + baseUrl: '../static/', paths: { 'mail_list': 'js/mail_list', 'page': 'js/page', diff --git a/web-ui/public/js/page/default.js b/web-ui/public/js/page/default.js index ecaedfd8..5d52e06c 100644 --- a/web-ui/public/js/page/default.js +++ b/web-ui/public/js/page/default.js @@ -96,7 +96,7 @@ define( 'use strict'; function initialize(path) { - viewI18n.init(path + '/assets/'); + viewI18n.init(path + '/static/'); viewI18n.loaded(function() { paneContractExpand.attachTo(document); diff --git a/web-ui/public/scss/base/_fonts.scss b/web-ui/public/scss/base/_fonts.scss index dfc56dd8..a8f2d7da 100644 --- a/web-ui/public/scss/base/_fonts.scss +++ b/web-ui/public/scss/base/_fonts.scss @@ -2,67 +2,67 @@ 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'); + src: local('Open Sans Light'), local('OpenSans-Light'), url('/static/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'); + src: local('Open Sans'), local('OpenSans'), url('/static/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'); + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url('/static/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'); + src: local('Open Sans Bold'), local('OpenSans-Bold'), url('/static/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'); + src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'), url('/static/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'); + src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url('/static/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'); + src: local('Open Sans Italic'), local('OpenSans-Italic'), url('/static/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'); + src: local('Open Sans Semibold Italic'), local('OpenSans-SemiboldItalic'), url('/static/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'); + src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url('/static/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'); + src: local('Open Sans Extrabold Italic'), local('OpenSans-ExtraboldItalic'), url('/static/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'), ; + src: url('/static/fonts/icomoon.woff') format('woff'), url('/static/fonts/icomoon.ttf') format('truetype'), ; } -- cgit v1.2.3 From 391cc55537a97ec8b2b55662db9c63f86ab885ef Mon Sep 17 00:00:00 2001 From: Roald de Vries Date: Mon, 5 Dec 2016 10:32:12 +0100 Subject: get templates from pkg_resources --- service/pixelated/adapter/welcome_mail.py | 2 +- service/pixelated/application.py | 20 ++-- service/pixelated/assets/Interstitial.html | 18 --- service/pixelated/assets/Interstitial.js | 58 ---------- service/pixelated/assets/__init__.py | 0 .../pixelated/assets/_login_disclaimer_banner.html | 9 -- service/pixelated/assets/favicon.png | Bin 592 -> 0 bytes service/pixelated/assets/hive-bg.png | Bin 3356 -> 0 bytes service/pixelated/assets/index.html | 9 -- service/pixelated/assets/jquery-2.1.3.min.js | 4 - service/pixelated/assets/login.html | 37 ------ service/pixelated/assets/normalize.min.css | 1 - service/pixelated/assets/opensans.css | 69 ----------- service/pixelated/assets/pixelated-logo-orange.svg | 29 ----- service/pixelated/assets/pixelated.css | 128 --------------------- service/pixelated/assets/snap.svg-min.js | 20 ---- service/pixelated/assets/welcome.mail.en-US | 94 --------------- service/pixelated/assets/welcome.mail.pt-BR | 102 ---------------- service/pixelated/assets/welcome.mail.pt-BR.txt | 30 ----- service/pixelated/resources/inbox_resource.py | 8 +- service/pixelated/resources/login_resource.py | 7 +- service/pixelated/resources/root_resource.py | 6 +- service/templates/Interstitial.html | 18 +++ service/templates/Interstitial.js | 58 ++++++++++ service/templates/_login_disclaimer_banner.html | 9 ++ service/templates/favicon.png | Bin 0 -> 592 bytes service/templates/hive-bg.png | Bin 0 -> 3356 bytes service/templates/index.html | 9 ++ service/templates/jquery-2.1.3.min.js | 4 + service/templates/login.html | 37 ++++++ service/templates/normalize.min.css | 1 + service/templates/opensans.css | 69 +++++++++++ service/templates/pixelated-logo-orange.svg | 29 +++++ service/templates/pixelated.css | 128 +++++++++++++++++++++ service/templates/snap.svg-min.js | 20 ++++ service/templates/welcome.mail.en-US | 94 +++++++++++++++ service/templates/welcome.mail.pt-BR | 102 ++++++++++++++++ service/templates/welcome.mail.pt-BR.txt | 30 +++++ service/test/integration/test_contacts.py | 1 - service/test/unit/test_welcome_mail.py | 3 +- 40 files changed, 629 insertions(+), 634 deletions(-) delete mode 100644 service/pixelated/assets/Interstitial.html delete mode 100644 service/pixelated/assets/Interstitial.js delete mode 100644 service/pixelated/assets/__init__.py delete mode 100644 service/pixelated/assets/_login_disclaimer_banner.html delete mode 100644 service/pixelated/assets/favicon.png delete mode 100644 service/pixelated/assets/hive-bg.png delete mode 100644 service/pixelated/assets/index.html delete mode 100644 service/pixelated/assets/jquery-2.1.3.min.js delete mode 100644 service/pixelated/assets/login.html delete mode 100644 service/pixelated/assets/normalize.min.css delete mode 100644 service/pixelated/assets/opensans.css delete mode 100644 service/pixelated/assets/pixelated-logo-orange.svg delete mode 100644 service/pixelated/assets/pixelated.css delete mode 100644 service/pixelated/assets/snap.svg-min.js delete mode 100644 service/pixelated/assets/welcome.mail.en-US delete mode 100644 service/pixelated/assets/welcome.mail.pt-BR delete mode 100644 service/pixelated/assets/welcome.mail.pt-BR.txt create mode 100644 service/templates/Interstitial.html create mode 100644 service/templates/Interstitial.js create mode 100644 service/templates/_login_disclaimer_banner.html create mode 100644 service/templates/favicon.png create mode 100644 service/templates/hive-bg.png create mode 100644 service/templates/index.html create mode 100644 service/templates/jquery-2.1.3.min.js create mode 100644 service/templates/login.html create mode 100644 service/templates/normalize.min.css create mode 100644 service/templates/opensans.css create mode 100644 service/templates/pixelated-logo-orange.svg create mode 100644 service/templates/pixelated.css create mode 100644 service/templates/snap.svg-min.js create mode 100644 service/templates/welcome.mail.en-US create mode 100644 service/templates/welcome.mail.pt-BR create mode 100644 service/templates/welcome.mail.pt-BR.txt diff --git a/service/pixelated/adapter/welcome_mail.py b/service/pixelated/adapter/welcome_mail.py index 8d3cdd7a..50147990 100644 --- a/service/pixelated/adapter/welcome_mail.py +++ b/service/pixelated/adapter/welcome_mail.py @@ -20,7 +20,7 @@ from pixelated.adapter.model.mail import InputMail def add_welcome_mail(mail_store, language='en-US'): welcome_mail = pkg_resources.resource_filename( - 'pixelated.assets', + 'templates', 'welcome.mail.%s' % (language)) with open(welcome_mail) as mail_template_file: diff --git a/service/pixelated/application.py b/service/pixelated/application.py index 0c2383dd..7263482d 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -42,6 +42,16 @@ from pixelated.resources.root_resource import RootResource log = Logger() +def get_templates_folder(): + return os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "templates") + + +def get_static_folder(): + # TODO: make sure sandbox keeps working + # TODO: make sure this works for packaging + return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "web-ui", "public")) + + class UserAgentMode(object): def __init__(self, is_single_user): self.is_single_user = is_single_user @@ -88,16 +98,6 @@ def _create_service_factory(args): return ServicesFactory(UserAgentMode(is_single_user=False)) -def get_templates_folder(): - return os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets") - - -def get_static_folder(): - # TODO: make sure sandbox keeps working - # TODO: make sure this works for packaging - return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "web-ui", "public")) - - def initialize(): log.info('Starting the Pixelated user agent') args = arguments.parse_user_agent_args() diff --git a/service/pixelated/assets/Interstitial.html b/service/pixelated/assets/Interstitial.html deleted file mode 100644 index bc6cc738..00000000 --- a/service/pixelated/assets/Interstitial.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - -
                  - -
                  - - - - diff --git a/service/pixelated/assets/Interstitial.js b/service/pixelated/assets/Interstitial.js deleted file mode 100644 index ac5a789a..00000000 --- a/service/pixelated/assets/Interstitial.js +++ /dev/null @@ -1,58 +0,0 @@ -if ($('#hive').length) { - var hive = new Snap('#hive'); - var img_width = $('#hive').width(); - var left_pos = img_width * .5; - - var pixelated = hive.path("M12.4,20.3v31.8l28,15.8l28-15.8V20.3l-28-15.8L12.4,20.3z M39.2,56.4l-16.3-9V27.9l16.3,9.3L39.2,56.4z M57.7,47.4l-16.1,9l0-19.2l16.1-9.4V47.4z M57.7,25.2L40.4,35.5L22.9,25.2l17.5-9.4L57.7,25.2z").transform("translate(319, 50)").attr("fill", "#908e8e"); - var all = hive.group().transform("matrix(2, 0, 0, 2, -100, -100)"); - - var height = 50; - var width = 58; - var rows = (($(window).height() / height) / 2) + 1; - var cols = (($(window).width() / width) / 2) + 1; - - - for (var j = 0; j < rows; j++) { - for (var i = 0; i < cols; i++) { - x = i * width + (j%2*width/2); - y = j * height; - all.add(pixelated.clone().transform("translate("+x+","+y+")")); - } - } - - all.add(pixelated); - - var brightenLogo = function () { - var glowPosition = Math.floor(Math.random()*rows*cols); - - all[glowPosition].animate({fill: "#FFF"}, 1000, function() { - darkenLogo(all[glowPosition]); - }); - }; - - var darkenLogo = function (el) { - el.animate({fill: "#908e8e"}, 1000, brightenLogo); - }; - - brightenLogo(); - -} - -$(function () { - var handler = setInterval(function () { - $.ajax({ - method: 'GET', - url: '/' - }).success(function (data) { - if (/Pixelated Mail/g.test(data)) { - window.location="/"; - } - }); - }, 2000); - - $('#hive-section').height($(window).height()); - - $(window).resize(function() { - window.location.reload(true); - }); -}); diff --git a/service/pixelated/assets/__init__.py b/service/pixelated/assets/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/service/pixelated/assets/_login_disclaimer_banner.html b/service/pixelated/assets/_login_disclaimer_banner.html deleted file mode 100644 index dfc63030..00000000 --- a/service/pixelated/assets/_login_disclaimer_banner.html +++ /dev/null @@ -1,9 +0,0 @@ -
                  -
                    -

                    Some disclaimer

                    -
                  • - please supply the option --banner with an XML compatible file -
                    to override this default message
                    -
                  • -
                  -
                  diff --git a/service/pixelated/assets/favicon.png b/service/pixelated/assets/favicon.png deleted file mode 100644 index e14841c7..00000000 Binary files a/service/pixelated/assets/favicon.png and /dev/null differ diff --git a/service/pixelated/assets/hive-bg.png b/service/pixelated/assets/hive-bg.png deleted file mode 100644 index 77316967..00000000 Binary files a/service/pixelated/assets/hive-bg.png and /dev/null differ diff --git a/service/pixelated/assets/index.html b/service/pixelated/assets/index.html deleted file mode 100644 index c095577e..00000000 --- a/service/pixelated/assets/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - click here - - - diff --git a/service/pixelated/assets/jquery-2.1.3.min.js b/service/pixelated/assets/jquery-2.1.3.min.js deleted file mode 100644 index 25714ed2..00000000 --- a/service/pixelated/assets/jquery-2.1.3.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c) -},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
                  "],col:[2,"","
                  "],tr:[2,"","
                  "],td:[3,"","
                  "],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("