diff --git a/package.json b/package.json index eda180c..35c72ff 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,17 @@ "type": "git", "url": "https://github.com/whiteout-io/mail-html5.git" }, - "keywords": ["email", "mail", "client", "app", "openpgp", "pgp", "gpg", "imap", "smtp"], + "keywords": [ + "email", + "mail", + "client", + "app", + "openpgp", + "pgp", + "gpg", + "imap", + "smtp" + ], "engines": { "node": ">=0.10" }, diff --git a/src/js/app.js b/src/js/app.js index 40b6465..0051cda 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -27,7 +27,8 @@ requirejs([ 'fastclick', 'angularRoute', 'angularAnimate', - 'ngInfiniteScroll' + 'ngInfiniteScroll', + 'ngTagsInput' ], function( angular, DialogCtrl, @@ -70,7 +71,8 @@ requirejs([ 'login-new-device', 'privatekey-upload', 'popover', - 'infinite-scroll' + 'infinite-scroll', + 'ngTagsInput' ]); // set router paths diff --git a/src/js/controller/write.js b/src/js/controller/write.js index d2f1e33..b9b4e9e 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -14,14 +14,13 @@ define(function(require) { // Controller // - var WriteCtrl = function($scope, $filter) { + var WriteCtrl = function($scope, $filter, $q) { pgp = appController._pgp; auth = appController._auth; emailDao = appController._emailDao; outbox = appController._outboxBo; keychainDao = appController._keychain; - // set default value so that the popover height is correct on init $scope.keyId = 'XXXXXXXX'; @@ -55,21 +54,16 @@ define(function(require) { function resetFields() { $scope.writerTitle = 'New email'; - $scope.to = [{ - address: '' - }]; + $scope.to = []; $scope.showCC = false; - $scope.cc = [{ - address: '' - }]; + $scope.cc = []; $scope.showBCC = false; - $scope.bcc = [{ - address: '' - }]; + $scope.bcc = []; $scope.subject = ''; $scope.body = ''; $scope.ciphertextPreview = ''; $scope.attachments = []; + $scope.addressBookCache = undefined; } function reportBug() { @@ -218,17 +212,13 @@ define(function(require) { // /** - * This event is fired when editing the email address headers. It checks is space is pressed and if so, creates a new address field. - */ - $scope.onAddressUpdate = function(field, index) { - var recipient = field[index]; - $scope.verify(recipient); - }; - - /** - * Verify and email address and fetch its public key + * Verify email address and fetch its public key */ $scope.verify = function(recipient) { + if (!recipient) { + return; + } + // set display to insecure while fetching keys recipient.key = undefined; recipient.secure = false; @@ -241,42 +231,32 @@ define(function(require) { return; } - // check if to address is contained in known public keys - // when we write an email, we always need to work with the latest keys available - keychainDao.refreshKeyForUserId(recipient.address, function(err, key) { - if (err) { - $scope.onError(err); - return; - } - - if (key) { - // compare again since model could have changed during the roundtrip - var matchingUserId = _.findWhere(key.userIds, { - emailAddress: recipient.address - }); - // compare either primary userId or (if available) multiple IDs - if (key.userId === recipient.address || matchingUserId) { - recipient.key = key; - recipient.secure = true; + // keychainDao is undefined in local dev environment + if (keychainDao) { + // check if to address is contained in known public keys + // when we write an email, we always need to work with the latest keys available + keychainDao.refreshKeyForUserId(recipient.address, function(err, key) { + if (err) { + $scope.onError(err); + return; } - } - $scope.checkSendStatus(); - $scope.$digest(); - }); - }; + if (key) { + // compare again since model could have changed during the roundtrip + var matchingUserId = _.findWhere(key.userIds, { + emailAddress: recipient.address + }); + // compare either primary userId or (if available) multiple IDs + if (key.userId === recipient.address || matchingUserId) { + recipient.key = key; + recipient.secure = true; + } + } - $scope.getKeyId = function(recipient) { - $scope.keyId = 'Key not found for that user.'; - - if (!recipient.key) { - return; + $scope.checkSendStatus(); + $scope.$digest(); + }); } - - var fpr = pgp.getFingerprint(recipient.key.publicKey); - var formatted = fpr.slice(32); - - $scope.keyId = formatted; }; /** @@ -418,6 +398,52 @@ define(function(require) { }; + // + // Tag input & Autocomplete + // + + $scope.tagStyle = function(recipient) { + var classes = ['label']; + if (recipient.secure === false) { + classes.push('label-primary'); + } + return classes; + }; + + $scope.lookupAddressBook = function(query) { + var deferred = $q.defer(); + + if (!$scope.addressBookCache) { + // populate address book cache + keychainDao.listLocalPublicKeys(function(err, keys) { + if (err) { + $scope.onError(err); + return; + } + + $scope.addressBookCache = keys.map(function(key) { + return { + address: key.userId + }; + }); + filter(); + }); + + } else { + filter(); + } + + // query address book cache + function filter() { + var addresses = $scope.addressBookCache.filter(function(i) { + return i.address.indexOf(query) !== -1; + }); + deferred.resolve(addresses); + } + + return deferred.promise; + }; + // // Helpers // @@ -489,138 +515,28 @@ define(function(require) { }; }); - ngModule.directive('autoSize', function($parse) { + ngModule.directive('focusInput', function($timeout, $parse) { return { - require: 'ngModel', - link: function(scope, elm, attrs) { - // resize text input depending on value length - var model = $parse(attrs.autoSize); - scope.$watch(model, function(value) { - var width; - - if (!value || value.length < 12) { - width = (14 * 8) + 'px'; - } else { - width = ((value.length + 2) * 8) + 'px'; - } - - elm.css('width', width); - }); - } - }; - }); - - function addInput(field, scope) { - scope.$apply(function() { - field.push({ - address: '' - }); - }); - } - - function removeInput(field, index, scope) { - scope.$apply(function() { - field.splice(index, 1); - }); - } - - function checkForEmptyInput(field) { - var emptyFieldExists = false; - field.forEach(function(recipient) { - if (!recipient.address) { - emptyFieldExists = true; - } - }); - return emptyFieldExists; - } - - function cleanupEmptyInputs(field, scope) { - scope.$apply(function() { - for (var i = field.length - 2; i >= 0; i--) { - if (!field[i].address) { - field.splice(i, 1); - } - } - }); - } - - function focusInput(fieldName, index) { - var fieldId = fieldName + (index); - var fieldEl = document.getElementById(fieldId); - if (fieldEl) { - fieldEl.focus(); - } - } - - ngModule.directive('field', function() { - return { - scope: true, + //scope: true, // optionally create a child scope link: function(scope, element, attrs) { - element.on('click', function(e) { - if (e.target.nodeName === 'INPUT') { - return; + var model = $parse(attrs.focusInput); + scope.$watch(model, function(value) { + if (value === true) { + $timeout(function() { + element.find('input').first().focus(); + }, 100); } - - var fieldName = attrs.field; - var field = scope[fieldName]; - - if (!checkForEmptyInput(field)) { - // create new field input if no empy one exists - addInput(field, scope); - } - - // focus on last input when clicking on field - focusInput(fieldName, field.length - 1); }); } }; }); - ngModule.directive('addressInput', function() { + ngModule.directive('focusInputOnClick', function() { return { - scope: true, - link: function(scope, elm, attrs) { - // get prefix for id - var fieldName = attrs.addressInput; - var field = scope[fieldName]; - var index = parseInt(attrs.id.replace(fieldName, ''), 10); - - elm.on('blur', function() { - if (!checkForEmptyInput(field)) { - // create new field input - addInput(field, scope); - } - - cleanupEmptyInputs(field, scope); - }); - - elm.on('keydown', function(e) { - var code = e.keyCode; - var address = elm[0].value; - - if (code === 32 || code === 188 || code === 186) { - // catch space, comma, semicolon - e.preventDefault(); - - // add next field only if current input is not empty - if (address) { - // create new field input - addInput(field, scope); - - // find next input and focus - focusInput(fieldName, index + 1); - } - - } else if ((code === 8 || code === 46) && !address && field.length > 1) { - // backspace, delete on empty input - // remove input - e.preventDefault(); - - removeInput(field, index, scope); - - // focus on previous id - focusInput(fieldName, index - 1); - } + //scope: true, // optionally create a child scope + link: function(scope, element) { + element.on('click', function() { + element.find('input').first().focus(); }); } }; diff --git a/src/lib/ngtagsinput/ng-tags-input.min.js b/src/lib/ngtagsinput/ng-tags-input.min.js new file mode 100755 index 0000000..340314a --- /dev/null +++ b/src/lib/ngtagsinput/ng-tags-input.min.js @@ -0,0 +1,5 @@ +/* + FROM FORKED REPO OF ngTagsInput: https://github.com/nanlabs/ngTagsInput + It adds tagStyle attribute for custom styling of tags. +*/ +/*! ngTagsInput v2.1.0 License: MIT */!function(){"use strict";function a(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(b,c){return angular.forEach(a[b],function(a){a.call(null,c)}),this}}}function b(a,b){return a=a||[],a.length>0&&!angular.isObject(a[0])&&a.forEach(function(c,d){a[d]={},a[d][b]=c}),a}function c(a,b,c){for(var d=null,f=0;f/g,">")}var g={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,comma:188},h=9007199254740991,i=["text","email","url"],j=angular.module("ngTagsInput",[]);j.directive("tagsInput",["$timeout","$document","tagsInputConfig",function(d,f,j){function k(a,b){var d,f,g,h={};return d=function(b){return e(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var e=d(b);return e&&e.length>=a.minLength&&e.length<=a.maxLength&&a.allowedTagsPattern.test(e)&&!c(h.items,b,a.displayProperty)},h.items=[],h.addText=function(a){var b={};return f(b,a),h.add(b)},h.add=function(c){var e=d(c);return a.replaceSpacesWithDashes&&(e=e.replace(/\s/g,"-")),f(c,e),g(c)?(h.items.push(c),b.trigger("tag-added",{$tag:c})):e&&b.trigger("invalid-tag",{$tag:c}),c},h.remove=function(a){var c=h.items.splice(a,1)[0];return b.trigger("tag-removed",{$tag:c}),c},h.removeLast=function(){var b,c=h.items.length-1;return a.enableEditingLastTag||h.selected?(h.selected=null,b=h.remove(c)):h.selected||(h.selected=h.items[c]),b},h}function l(a){return-1!==i.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",onTagAdded:"&",onTagRemoved:"&",tagStyle:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(b,c,d){b.events=new a,j.load("tagsInput",b,c,{type:[String,"text",l],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,h],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,h],displayProperty:[String,"text"],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1]}),b.tagList=new k(b.options,b.events),this.registerAutocomplete=function(){var a=d.find("input");return a.on("keydown",function(a){b.events.trigger("input-keydown",a)}),{addTag:function(a){return b.tagList.add(a)},focusInput:function(){a[0].focus()},getTags:function(){return b.tags},getCurrentTagText:function(){return b.newTag.text},getOptions:function(){return b.options},on:function(a,c){return b.events.on(a,c),this}}}}],link:function(a,c,h,i){var j,k=[g.enter,g.comma,g.space,g.backspace],l=a.tagList,m=a.events,n=a.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",a.tags.length<=n.maxTags),i.$setValidity("minTags",a.tags.length>=n.minTags),i.$setValidity("leftoverText",n.allowLeftoverText?!0:!a.newTag.text)},m.on("tag-added",a.onTagAdded).on("tag-removed",a.onTagRemoved).on("tag-added",function(){a.newTag.text=""}).on("tag-added tag-removed",function(){i.$setViewValue(a.tags)}).on("invalid-tag",function(){a.newTag.invalid=!0}).on("input-change",function(){l.selected=null,a.newTag.invalid=null}).on("input-focus",function(){i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addFromAutocompleteOnly||(n.addOnBlur&&l.addText(a.newTag.text),j())}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}),a.newTag={text:"",invalid:null},a.getTagStyles=function(b,c,d){var e=[];return b===c.selected&&e.push("selected"),a.tagStyle()&&(e=e.concat(a.tagStyle()(b,c,d))),e},a.getDisplayText=function(a){return e(a[n.displayProperty])},a.track=function(a){return a[n.displayProperty]},a.newTagChange=function(){m.trigger("input-change",a.newTag.text)},a.$watch("tags",function(c){a.tags=b(c,n.displayProperty),l.items=a.tags}),a.$watch("tags.length",function(){j()}),o.on("keydown",function(b){if(!b.isImmediatePropagationStopped||!b.isImmediatePropagationStopped()){var c,d,e=b.keyCode,f=b.shiftKey||b.altKey||b.ctrlKey||b.metaKey,h={};if(!f&&-1!==k.indexOf(e))if(h[g.enter]=n.addOnEnter,h[g.comma]=n.addOnComma,h[g.space]=n.addOnSpace,c=!n.addFromAutocompleteOnly&&h[e],d=!c&&e===g.backspace&&0===a.newTag.text.length,c)l.addText(a.newTag.text),a.$apply(),b.preventDefault();else if(d){var i=l.removeLast();i&&n.enableEditingLastTag&&(a.newTag.text=i[n.displayProperty]),a.$apply(),b.preventDefault()}}}).on("focus",function(){a.hasFocus||(a.hasFocus=!0,m.trigger("input-focus"),a.$apply())}).on("blur",function(){d(function(){var b=f.prop("activeElement"),d=b===o[0],e=c[0].contains(b);(d||!e)&&(a.hasFocus=!1,m.trigger("input-blur"))})}),c.find("div").on("click",function(){o[0].focus()})}}}]),j.directive("autoComplete",["$document","$timeout","$sce","tagsInputConfig",function(a,h,i,j){function k(a,d){var e,f,g,i={};return f=function(a,b){return a.filter(function(a){return!c(b,a,d.tagsInput.displayProperty)})},i.reset=function(){g=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null,h.cancel(e)},i.show=function(){i.selected=null,i.visible=!0},i.load=function(c,j){h.cancel(e),e=h(function(){i.query=c;var e=a({$query:c});g=e,e.then(function(a){e===g&&(a=b(a.data||a,d.tagsInput.displayProperty),a=f(a,j),i.items=a.slice(0,d.maxResultsToShow),i.items.length>0?i.show():i.reset())})},d.debounceDelay,!1)},i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.reset(),i}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",link:function(a,b,c,h){var l,m,n,o,p,q,r=[g.enter,g.tab,g.escape,g.up,g.down];j.load("autoComplete",a,c,{debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1]}),n=a.options,m=h.registerAutocomplete(),n.tagsInput=m.getOptions(),l=new k(a.source,n),o=function(a){return a[n.tagsInput.displayProperty]},p=function(a){return e(o(a))},q=function(a){return a&&a.length>=n.minLength||!a&&n.loadOnEmpty},a.suggestionList=l,a.addSuggestionByIndex=function(b){l.select(b),a.addSuggestion()},a.addSuggestion=function(){var a=!1;return l.selected&&(m.addTag(l.selected),l.reset(),m.focusInput(),a=!0),a},a.highlight=function(a){var b=p(a);return b=f(b),n.highlightMatchedText&&(b=d(b,f(l.query),"$&")),i.trustAsHtml(b)},a.track=function(a){return o(a)},m.on("tag-added tag-removed invalid-tag input-blur",function(){l.reset()}).on("input-change",function(a){q(a)?l.load(a,m.getTags()):l.reset()}).on("input-focus",function(){var a=m.getCurrentTagText();n.loadOnFocus&&q(a)&&l.load(a,m.getTags())}).on("input-keydown",function(b){var c=!1;b.stopImmediatePropagation=function(){c=!0,b.stopPropagation()},b.isImmediatePropagationStopped=function(){return c};var d=b.keyCode,e=!1;-1!==r.indexOf(d)&&(l.visible?d===g.down?(l.selectNext(),e=!0):d===g.up?(l.selectPrior(),e=!0):d===g.escape?(l.reset(),e=!0):(d===g.enter||d===g.tab)&&(e=a.addSuggestion()):d===g.down&&a.options.loadOnDownArrow&&(l.load(m.getCurrentTagText(),m.getTags()),e=!0),e&&(b.preventDefault(),b.stopImmediatePropagation(),a.$apply()))})}}}]),j.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),j.directive("tiAutosize",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){var e,f,g=3;e=angular.element(''),e.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),b.parent().append(e),f=function(a){var d,f=a;return angular.isString(f)&&0===f.length&&(f=c.placeholder),f&&(e.text(f),e.css("display",""),d=e.prop("offsetWidth"),e.css("display","none")),b.css("width",d?d+g+"px":""),a},d.$parsers.unshift(f),d.$formatters.unshift(f),c.$observe("placeholder",function(a){d.$modelValue||f(a)})}}}),j.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),j.provider("tagsInputConfig",function(){var a={},b={};this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.$get=["$interpolate",function(c){var d={};return d[String]=function(a){return a},d[Number]=function(a){return parseInt(a,10)},d[Boolean]=function(a){return"true"===a.toLowerCase()},d[RegExp]=function(a){return new RegExp(a)},{load:function(e,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=d[k],o=function(){var b=a[e]&&a[e][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[e]&&b[e][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&c(g[j])(f.$parent))})}}}]}),j.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
'),a.put("ngTagsInput/auto-complete.html",'
')}])}(); \ No newline at end of file diff --git a/src/require-config.js b/src/require-config.js index 1b1054f..4a550a6 100644 --- a/src/require-config.js +++ b/src/require-config.js @@ -16,6 +16,7 @@ angularRoute: 'angular/angular-route.min', angularAnimate: 'angular/angular-animate.min', ngInfiniteScroll: 'ng-infinite-scroll.min', + ngTagsInput: 'ngtagsinput/ng-tags-input.min', uuid: 'uuid/uuid', forge: 'forge/forge.min', punycode: 'punycode.min', @@ -45,6 +46,10 @@ exports: 'angular', deps: ['jquery', 'angular'] }, + ngTagsInput: { + exports: 'angular', + deps: ['angular'] + }, lawnchair: { exports: 'Lawnchair' }, diff --git a/src/sass/_variables.scss b/src/sass/_variables.scss index 866d4ed..1401416 100755 --- a/src/sass/_variables.scss +++ b/src/sass/_variables.scss @@ -14,9 +14,10 @@ $line-height-base: 20 / 16; // Colors // ------------------------------------------- -$color-blue: #00c6ff; $color-black: #000; $color-white: #fff; +$color-blue: #00c6ff; +$color-red: #ff878d; $color-grey: #666; $color-grey-input: #949494; $color-grey-dark: #333; @@ -73,7 +74,7 @@ $label-font-size: $font-size-small; $label-padding-horizontal: 0.8em; $label-padding-vertical: 0.3em; -$label-primary-back-color: #ff878d; +$label-primary-back-color: $color-red; $label-primary-color: #fff; $label-light-back-color: #fff; diff --git a/src/sass/all.scss b/src/sass/all.scss index ebdd46d..137d901 100755 --- a/src/sass/all.scss +++ b/src/sass/all.scss @@ -28,6 +28,7 @@ @import "components/mail-addresses"; @import "components/spinner"; @import "components/scrollbars"; +@import "components/tags-input"; // Views @import "views/shared"; diff --git a/src/sass/components/_mail-addresses.scss b/src/sass/components/_mail-addresses.scss index bf5b55e..88313e1 100644 --- a/src/sass/components/_mail-addresses.scss +++ b/src/sass/components/_mail-addresses.scss @@ -1,7 +1,6 @@ .mail-addresses { p { margin: 0.4em 0 0.2em; - cursor: text; } .label { diff --git a/src/sass/components/_tags-input.scss b/src/sass/components/_tags-input.scss new file mode 100644 index 0000000..34e1332 --- /dev/null +++ b/src/sass/components/_tags-input.scss @@ -0,0 +1,87 @@ +// Styling for ngTagsInput +.tags-input { + + .host { + position: relative; + &:active { + outline: none; + } + } + + .tags { + overflow: hidden; + word-wrap: break-word; + cursor: text; + } + .tag-list { + margin: 0; + padding: 0; + list-style-type: none; + float: left; + } + .tag-item { + cursor: default; + + .remove-button { + font-weight: bold; + font-size: 1.2em; + vertical-align: middle; + line-height: 0.5em; + cursor: pointer; + } + } + .tags .input { + border: 0; + outline: none; + margin: 2px; + padding: 0; + padding-left: 5px; + float: left; + min-width: 10em; + + &.invalid-tag { + color: $color-red; + } + &::-ms-clear { + display: none; + } + } + + li.ng-animate { + transition: none; + } + + .autocomplete { + margin-top: 5px; + position: absolute; + z-index: 999; + width: 100%; + background-color: white; + border: 1px solid $color-grey-lighter; + box-shadow: 2px 2px 5px rgba($color-black, 0.2); + + .suggestion-list { + margin: 0; + padding: 0; + list-style-type: none; + } + .suggestion-item { + padding: 5px 10px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: $color-black; + background-color: $color-white; + + em { + font-weight: bold; + font-style: normal; + } + &.selected { + color: white; + background-color: $color-blue; + } + } + } +} \ No newline at end of file diff --git a/src/sass/views/_write.scss b/src/sass/views/_write.scss index 179c035..14fbb94 100644 --- a/src/sass/views/_write.scss +++ b/src/sass/views/_write.scss @@ -20,27 +20,44 @@ .mail-addresses { flex-shrink: 0; margin-top: 10px; - } - .mail-addresses-more { - float: right; - margin: 0.4em 0; + p { + position: relative; + padding-left: 3em; + } + label { + position: absolute; + top: 0; + left: 0; + width: 3em; + } - button { - display: inline-block; - background: none; - padding: 0 0.5em; - margin: 0; - text-decoration: none; - color: $color-black; - transition: color 0.3s; - border: 0; - outline: 0; + .mail-addresses-more { + position: relative; + float: right; + margin: 0.4em 0; + z-index: 1; - &:hover, - &:focus { - color: $color-blue; - text-decoration: underline; + button { + display: inline-block; + background: none; + padding: 0 0.5em; + margin: 0; + text-decoration: none; + color: $color-black; + transition: color 0.3s; + border: 0; + outline: 0; + + &:hover, + &:focus { + color: $color-blue; + text-decoration: underline; + } + + &.ng-animate { + transition: none; + } } } } diff --git a/src/tpl/write.html b/src/tpl/write.html index 697560c..2baa4a9 100644 --- a/src/tpl/write.html +++ b/src/tpl/write.html @@ -10,23 +10,37 @@ -

+

- - - + + +

-

+

- - - + + +

-

+

- - - + + +

diff --git a/test/unit/write-ctrl-test.js b/test/unit/write-ctrl-test.js index 3492b7b..76b811b 100644 --- a/test/unit/write-ctrl-test.js +++ b/test/unit/write-ctrl-test.js @@ -63,10 +63,11 @@ define(function(require) { expect(scope.state.writer.write).to.exist; expect(scope.state.writer.close).to.exist; expect(scope.verify).to.exist; - expect(scope.onAddressUpdate).to.exist; expect(scope.checkSendStatus).to.exist; expect(scope.updatePreview).to.exist; expect(scope.sendToOutbox).to.exist; + expect(scope.tagStyle).to.exist; + expect(scope.lookupAddressBook).to.exist; }); }); @@ -87,9 +88,7 @@ define(function(require) { scope.state.writer.write(); expect(scope.writerTitle).to.equal('New email'); - expect(scope.to).to.deep.equal([{ - address: '' - }]); + expect(scope.to).to.deep.equal([]); expect(scope.subject).to.equal(''); expect(scope.body).to.equal(''); expect(scope.ciphertextPreview).to.equal(undefined); @@ -121,8 +120,6 @@ define(function(require) { expect(scope.writerTitle).to.equal('Reply'); expect(scope.to).to.deep.equal([{ address: address, - }, { - address: '' }]); expect(scope.subject).to.equal('Re: ' + subject); expect(scope.body).to.contain(body); @@ -156,9 +153,7 @@ define(function(require) { scope.state.writer.write(re, null, true); expect(scope.writerTitle).to.equal('Forward'); - expect(scope.to).to.deep.equal([{ - address: '' - }]); + expect(scope.to).to.deep.equal([]); expect(scope.subject).to.equal('Fwd: ' + subject); expect(scope.body).to.contain(body); expect(scope.ciphertextPreview).to.be.undefined; @@ -171,29 +166,6 @@ define(function(require) { }); - describe('onAddressUpdate', function() { - var verifyMock; - - beforeEach(function() { - verifyMock = sinon.stub(scope, 'verify'); - }); - - afterEach(function() { - scope.verify.restore(); - }); - - it('should do nothing for normal address', function() { - var to = [{ - address: 'asdf@asdf.de' - }]; - scope.onAddressUpdate(to, 0); - - expect(to.length).to.equal(1); - expect(to[0].address).to.equal('asdf@asdf.de'); - expect(verifyMock.calledOnce).to.be.true; - }); - }); - describe('verify', function() { var checkSendStatusMock; @@ -205,6 +177,10 @@ define(function(require) { scope.checkSendStatus.restore(); }); + it('should do nothing if recipient is not provided', function() { + scope.verify(undefined); + }); + it('should not work for invalid email addresses', function() { var recipient = { address: '' @@ -387,5 +363,43 @@ define(function(require) { expect(scope.replyTo.answered).to.be.true; }); }); + + describe('lookupAddressBook', function() { + it('should work', function(done) { + keychainMock.listLocalPublicKeys.yields(null, [{ + userId: 'test@asdf.com', + publicKey: 'KEY' + }]); + + var result = scope.lookupAddressBook('test'); + + result.then(function(response) { + expect(response).to.deep.equal([{ + address: 'test@asdf.com' + }]); + done(); + }); + scope.$digest(); + }); + + it('should work with cache', function(done) { + scope.addressBookCache = [{ + address: 'test@asdf.com' + }, { + address: 'tes@asdf.com' + }]; + + var result = scope.lookupAddressBook('test'); + + result.then(function(response) { + expect(response).to.deep.equal([{ + address: 'test@asdf.com' + }]); + done(); + }); + scope.$digest(); + }); + }); + }); }); \ No newline at end of file