diff options
21 files changed, 203 insertions, 87 deletions
diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 58594069..00000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.2.3 diff --git a/osx_setup.sh b/osx_setup.sh index a8a37833..f899b227 100644 --- a/osx_setup.sh +++ b/osx_setup.sh @@ -1,5 +1,35 @@ #!/bin/bash +# Test to make sure we are OSX +if [ $(uname) != 'Darwin' ] +then + echo "This script should run only on an OSX system!" + exit 1 +fi + +# Read the shell configured for the user and set the variable file accordingly +function current_shell { + case $SHELL in + *bash) + echo ~/.bash_profile + ;; + + *zsh) + echo ~/.zprofile + ;; + + /bin/sh) + echo ~/.profile + ;; + + *) + echo "Your shell isn't supported yet!" + ;; + + #Other shells can go here + esac +} + function install_compass { rbenv install -s 2.2.3 eval "$(rbenv init -)" @@ -7,8 +37,10 @@ function install_compass { rbenv local 2.2.3 gem install compass export PATH=$PATH:~/.rbenv/versions/2.2.3/bin - echo "export PATH=$PATH:~/.rbenv/versions/2.2.3/bin" >> ~/.bash_profile - echo 'eval "$(rbenv init -)"' >> ~/.bash_profile + echo "export PATH=$PATH:~/.rbenv/versions/2.2.3/bin" >> $(current_shell) + echo 'eval "$(rbenv init -)"' >> $(current_shell) + echo "export PATH=$PATH:~/.rbenv/versions/2.2.3/bin" >> $(current_shell) + echo 'eval "$(rbenv init -)"' >> $(current_shell) } function install_rbenv { @@ -30,13 +62,14 @@ function clone_repo { cd pixelated-user-agent fi } + #setup frontend install_rbenv install_compass install_npm #setup backend -brew install python # force brew install even if python is already installed +brew install python # force brew install even if python is already install export LDFLAGS=-L/usr/local/opt/openssl/lib export LDFLAGS=-L/usr/local/opt/openssl/lib pip install virtualenv diff --git a/service/pixelated/adapter/mailstore/leap_mailstore.py b/service/pixelated/adapter/mailstore/leap_mailstore.py index 975bcc5c..cd4cb5b8 100644 --- a/service/pixelated/adapter/mailstore/leap_mailstore.py +++ b/service/pixelated/adapter/mailstore/leap_mailstore.py @@ -27,6 +27,7 @@ from pixelated.adapter.mailstore.mailstore import MailStore, underscore_uuid from pixelated.adapter.model.mail import Mail, InputMail from pixelated.support import log_time_deferred from pixelated.support.functional import to_unicode +from pixelated.support import date MIME_PGP_KEY = 'application/pgp-keys' diff --git a/service/pixelated/adapter/mailstore/maintenance/__init__.py b/service/pixelated/adapter/mailstore/maintenance/__init__.py index edc442c2..9b6d6023 100644 --- a/service/pixelated/adapter/mailstore/maintenance/__init__.py +++ b/service/pixelated/adapter/mailstore/maintenance/__init__.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -from leap.keymanager.keys import KEY_TYPE_KEY, KEY_PRIVATE_KEY, KEY_ID_KEY, KEY_ADDRESS_KEY +from leap.keymanager.keys import KEY_TYPE_KEY, KEY_PRIVATE_KEY, KEY_FINGERPRINT_KEY, KEY_ADDRESS_KEY from leap.keymanager.openpgp import OpenPGPKey from twisted.internet import defer @@ -44,8 +44,8 @@ def _is_public_key(doc): return _is_key_doc(doc) and not doc.content.get(KEY_PRIVATE_KEY, False) -def _key_id(doc): - return doc.content.get(KEY_ID_KEY, None) +def _key_fingerprint(doc): + return doc.content.get(KEY_FINGERPRINT_KEY, None) def _address(doc): @@ -60,40 +60,41 @@ class SoledadMaintenance(object): def repair(self): _, docs = yield self._soledad.get_all_docs() - private_key_ids = self._key_ids_with_private_key(docs) + private_key_fingerprints = self._key_fingerprints_with_private_key(docs) for doc in docs: - if _is_key_doc(doc) and _key_id(doc) not in private_key_ids: - logger.warn('Deleting doc %s for key %s of <%s>' % (doc.doc_id, _key_id(doc), _address(doc))) + if _is_key_doc(doc) and _key_fingerprint(doc) not in private_key_fingerprints: + logger.warn('Deleting doc %s for key %s of <%s>' % (doc.doc_id, _key_fingerprint(doc), _address(doc))) yield self._soledad.delete_doc(doc) - yield self._repair_missing_active_docs(docs, private_key_ids) + yield self._repair_missing_active_docs(docs, private_key_fingerprints) @defer.inlineCallbacks - def _repair_missing_active_docs(self, docs, private_key_ids): - missing = self._missing_active_docs(docs, private_key_ids) - for key_id in missing: - emails = self._emails_for_key_id(docs, key_id) + def _repair_missing_active_docs(self, docs, private_key_fingerprints): + missing = self._missing_active_docs(docs, private_key_fingerprints) + for fingerprint in missing: + emails = self._emails_for_key_fingerprint(docs, fingerprint) for email in emails: - logger.warn('Re-creating active doc for key %s, email %s' % (key_id, email)) - yield self._soledad.create_doc_from_json(OpenPGPKey(email, key_id=key_id, private=False).get_active_json(email)) + logger.warn('Re-creating active doc for key %s, email %s' % (fingerprint, email)) + yield self._soledad.create_doc_from_json(OpenPGPKey(email, fingerprint=fingerprint, private=False).get_active_json()) - def _key_ids_with_private_key(self, docs): - return [doc.content[KEY_ID_KEY] for doc in docs if _is_private_key_doc(doc)] + def _key_fingerprints_with_private_key(self, docs): + return [doc.content[KEY_FINGERPRINT_KEY] for doc in docs if _is_private_key_doc(doc)] - def _missing_active_docs(self, docs, private_key_ids): - active_doc_ids = self._active_docs_for_key_id(docs) + def _missing_active_docs(self, docs, private_key_fingerprints): + active_doc_ids = self._active_docs_for_key_fingerprint(docs) - return set([private_key_id for private_key_id in private_key_ids if private_key_id not in active_doc_ids]) + return set([private_key_fingerprint for private_key_fingerprint in private_key_fingerprints if private_key_fingerprint not in active_doc_ids]) - def _emails_for_key_id(self, docs, key_id): + def _emails_for_key_fingerprint(self, docs, fingerprint): for doc in docs: - if _is_private_key_doc(doc) and _key_id(doc) == key_id: + if _is_private_key_doc(doc) and _key_fingerprint(doc) == fingerprint: email = _address(doc) + if email is None: + return [] if isinstance(email, list): return email - else: - return [email] + return [email] - def _active_docs_for_key_id(self, docs): - return [doc.content[KEY_ID_KEY] for doc in docs if _is_active_key_doc(doc) and _is_public_key(doc)] + def _active_docs_for_key_fingerprint(self, docs): + return [doc.content[KEY_FINGERPRINT_KEY] for doc in docs if _is_active_key_doc(doc) and _is_public_key(doc)] diff --git a/service/pixelated/adapter/search/__init__.py b/service/pixelated/adapter/search/__init__.py index e137b392..3ec6532b 100644 --- a/service/pixelated/adapter/search/__init__.py +++ b/service/pixelated/adapter/search/__init__.py @@ -30,6 +30,7 @@ from whoosh.writing import AsyncWriter from whoosh import sorting from pixelated.support.functional import unique, to_unicode import traceback +from pixelated.support import date class SearchEngine(object): @@ -128,7 +129,7 @@ class SearchEngine(object): index_data = { 'sender': self._empty_string_to_none(header.get('from', '')), 'subject': self._empty_string_to_none(header.get('subject', '')), - 'date': self._format_utc_integer(header.get('date', '')), + 'date': self._format_utc_integer(header.get('date', date.mail_date_now())), 'to': self._format_recipient(header, 'to'), 'cc': self._format_recipient(header, 'cc'), 'bcc': self._format_recipient(header, 'bcc'), diff --git a/service/pixelated/resources/keys_resource.py b/service/pixelated/resources/keys_resource.py index d6f469fe..9075ab9e 100644 --- a/service/pixelated/resources/keys_resource.py +++ b/service/pixelated/resources/keys_resource.py @@ -17,7 +17,7 @@ class KeysResource(BaseResource): if key.private: respond_json_deferred(None, request, status_code=401) else: - respond_json_deferred(key.get_json(), request) + respond_json_deferred(key.get_active_json(), request) def key_not_found(_): respond_json_deferred(None, request, status_code=404) diff --git a/service/pixelated/resources/logout_resource.py b/service/pixelated/resources/logout_resource.py index 344ad2e9..01092b05 100644 --- a/service/pixelated/resources/logout_resource.py +++ b/service/pixelated/resources/logout_resource.py @@ -1,5 +1,8 @@ +from twisted.web.server import NOT_DONE_YET + from pixelated.resources import BaseResource from twisted.web import util +from twisted.internet import defer from pixelated.resources.login_resource import LoginResource @@ -8,9 +11,19 @@ class LogoutResource(BaseResource): BASE_URL = "logout" isLeaf = True - def render_POST(self, request): + @defer.inlineCallbacks + def _execute_logout(self, request): session = self.get_session(request) - self._services_factory.log_out_user(session.user_uuid) + yield self._services_factory.log_out_user(session.user_uuid) session.expire() - return util.redirectTo("/%s" % LoginResource.BASE_URL, request) + def render_POST(self, request): + def _redirect_to_login(_): + content = util.redirectTo("/%s" % LoginResource.BASE_URL, request) + request.write(content) + request.finish() + + d = self._execute_logout(request) + d.addCallback(_redirect_to_login) + + return NOT_DONE_YET diff --git a/service/requirements.txt b/service/requirements.txt index 1966f09a..95a0f4f8 100644 --- a/service/requirements.txt +++ b/service/requirements.txt @@ -8,11 +8,11 @@ requests==2.0.0 srp==1.0.4 whoosh==2.5.7 pycryptopp --e 'git+https://github.com/pixelated-project/leap_pycommon.git@develop#egg=leap.common' --e 'git+https://github.com/pixelated-project/leap_auth.git#egg=leap.auth' --e 'git+https://github.com/pixelated-project/soledad.git@develop#egg=leap.soledad.common&subdirectory=common/' --e 'git+https://github.com/pixelated-project/soledad.git@develop#egg=leap.soledad.client&subdirectory=client/' --e 'git+https://github.com/pixelated-project/soledad.git@develop#egg=leap.soledad.server&subdirectory=server/' --e 'git+https://github.com/pixelated-project/keymanager.git@develop#egg=leap.keymanager' --e 'git+https://github.com/pixelated-project/leap_mail.git@develop#egg=leap.mail' +-e 'git+https://github.com/pixelated/leap_pycommon.git@develop#egg=leap.common' +-e 'git+https://github.com/pixelated/leap_auth.git#egg=leap.auth' +-e 'git+https://github.com/pixelated/soledad.git@develop#egg=leap.soledad.common&subdirectory=common/' +-e 'git+https://github.com/pixelated/soledad.git@develop#egg=leap.soledad.client&subdirectory=client/' +-e 'git+https://github.com/pixelated/soledad.git@develop#egg=leap.soledad.server&subdirectory=server/' +-e 'git+https://github.com/pixelated/keymanager.git@develop#egg=leap.keymanager' +-e 'git+https://github.com/pixelated/leap_mail.git@develop#egg=leap.mail' -e . diff --git a/service/test/unit/adapter/mailstore/maintenance/test_soledad_maintenance.py b/service/test/unit/adapter/mailstore/maintenance/test_soledad_maintenance.py index e46d6864..be73af93 100644 --- a/service/test/unit/adapter/mailstore/maintenance/test_soledad_maintenance.py +++ b/service/test/unit/adapter/mailstore/maintenance/test_soledad_maintenance.py @@ -26,7 +26,7 @@ logging.getLogger('pixelated.adapter.mailstore.maintenance').addHandler(logging. SOME_EMAIL_ADDRESS = 'foo@example.tld' -SOME_KEY_ID = '4914254E384E264C' +SOME_FINGERPRINT = '4914254E384E264C' class TestSoledadMaintenance(unittest.TestCase): @@ -42,8 +42,8 @@ class TestSoledadMaintenance(unittest.TestCase): @defer.inlineCallbacks def test_repair_delete_public_key_active_docs(self): soledad = mock() - key = self._public_key(SOME_EMAIL_ADDRESS, SOME_KEY_ID) - active_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json(SOME_EMAIL_ADDRESS)) + key = self._public_key(SOME_EMAIL_ADDRESS, SOME_FINGERPRINT) + active_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json()) when(soledad).get_all_docs().thenReturn(defer.succeed((1, [active_doc]))) yield SoledadMaintenance(soledad).repair() @@ -53,8 +53,8 @@ class TestSoledadMaintenance(unittest.TestCase): @defer.inlineCallbacks def test_repair_delete_public_key_docs(self): soledad = mock() - key = self._public_key(SOME_EMAIL_ADDRESS, SOME_KEY_ID) - active_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json(SOME_EMAIL_ADDRESS)) + key = self._public_key(SOME_EMAIL_ADDRESS, SOME_FINGERPRINT) + active_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json()) key_doc = SoledadDocument(doc_id='some_doc', json=key.get_json()) when(soledad).get_all_docs().thenReturn(defer.succeed((1, [key_doc, active_doc]))) @@ -66,9 +66,9 @@ class TestSoledadMaintenance(unittest.TestCase): @defer.inlineCallbacks def test_repair_keeps_active_and_key_doc_if_private_key_exists(self): soledad = mock() - key = self._public_key(SOME_EMAIL_ADDRESS, SOME_KEY_ID) - private_key = self._private_key(SOME_EMAIL_ADDRESS, SOME_KEY_ID) - active_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json(SOME_EMAIL_ADDRESS)) + key = self._public_key(SOME_EMAIL_ADDRESS, SOME_FINGERPRINT) + private_key = self._private_key(SOME_EMAIL_ADDRESS, SOME_FINGERPRINT) + active_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json()) key_doc = SoledadDocument(doc_id='some_doc', json=key.get_json()) private_key_doc = SoledadDocument(doc_id='some_doc', json=private_key.get_json()) when(soledad).get_all_docs().thenReturn(defer.succeed((1, [key_doc, active_doc, private_key_doc]))) @@ -82,8 +82,8 @@ class TestSoledadMaintenance(unittest.TestCase): @defer.inlineCallbacks def test_repair_only_deletes_key_docs(self): soledad = mock() - key = self._public_key(SOME_EMAIL_ADDRESS, SOME_KEY_ID) - key_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json(SOME_EMAIL_ADDRESS)) + key = self._public_key(SOME_EMAIL_ADDRESS, SOME_FINGERPRINT) + key_doc = SoledadDocument(doc_id='some_doc', json=key.get_active_json()) other_doc = SoledadDocument(doc_id='something', json='{}') when(soledad).get_all_docs().thenReturn(defer.succeed((1, [key_doc, other_doc]))) @@ -95,19 +95,19 @@ class TestSoledadMaintenance(unittest.TestCase): def test_repair_recreates_public_key_active_doc_if_necessary(self): soledad = mock() - private_key = self._private_key(SOME_EMAIL_ADDRESS, SOME_KEY_ID) - private_key_doc = SoledadDocument(doc_id='some_doc', json=private_key.get_json()) + private_key = self._private_key(SOME_EMAIL_ADDRESS, SOME_FINGERPRINT) + private_key_doc = SoledadDocument(doc_id='some_doc', json=private_key.get_active_json()) when(soledad).get_all_docs().thenReturn(defer.succeed((1, [private_key_doc]))) yield SoledadMaintenance(soledad).repair() - verify(soledad).create_doc_from_json('{"key_id": "4914254E384E264C", "tags": ["keymanager-active"], "type": "OpenPGPKey-active", "private": false, "address": "foo@example.tld"}') + verify(soledad).create_doc_from_json('{"encr_used": false, "sign_used": false, "validation": "Weak_Chain", "version": 1, "address": "foo@example.tld", "last_audited_at": 0, "fingerprint": "4914254E384E264C", "type": "OpenPGPKey-active", "private": false, "tags": ["keymanager-active"]}') - def _public_key(self, address, keyid): - return self._gpgkey(address, keyid, private=False) + def _public_key(self, address, fingerprint): + return self._gpgkey(address, fingerprint, private=False) - def _private_key(self, address, keyid): - return self._gpgkey(address, keyid, private=True) + def _private_key(self, address, fingerprint): + return self._gpgkey(address, fingerprint, private=True) - def _gpgkey(self, address, keyid, private=False): - return OpenPGPKey(address, key_id=keyid, private=private) + def _gpgkey(self, address, fingerprint, private=False): + return OpenPGPKey(address, fingerprint=fingerprint, private=private) diff --git a/service/test/unit/adapter/search/test_search.py b/service/test/unit/adapter/search/test_search.py index 198a818b..be37257c 100644 --- a/service/test/unit/adapter/search/test_search.py +++ b/service/test/unit/adapter/search/test_search.py @@ -20,6 +20,7 @@ import unittest from pixelated.adapter.mailstore.leap_mailstore import LeapMail from pixelated.adapter.search import SearchEngine from tempdir import TempDir +from datetime import datetime from test.support import test_helper from pixelated.support.functional import to_unicode @@ -57,6 +58,7 @@ class SearchEngineTest(unittest.TestCase): 'To': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=', 'Cc': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=', 'Subject': 'Some test mail', + 'Date': str(datetime.now()) } # when @@ -75,6 +77,7 @@ class SearchEngineTest(unittest.TestCase): 'To': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=', 'Cc': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=', 'Subject': 'Some test mail', + 'Date': str(datetime.now()) } body = "When doing the search, it's not possible to find words with graphical accents, e.g.: 'coração', 'é', 'Fièvre', La Pluie d'été, 'não'." @@ -100,6 +103,7 @@ class SearchEngineTest(unittest.TestCase): 'To': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=', 'Cc': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=', 'Subject': 'Some test mail', + 'Date': str(datetime.now()) } body = "When doing the search, 您好 أهلا" diff --git a/service/test/unit/resources/test_keys_resources.py b/service/test/unit/resources/test_keys_resources.py index 6aa822e1..2bf53cb4 100644 --- a/service/test/unit/resources/test_keys_resources.py +++ b/service/test/unit/resources/test_keys_resources.py @@ -44,20 +44,16 @@ class TestKeysResource(unittest.TestCase): d = self.web.get(request) expected = { - "tags": ["keymanager-key"], - "fingerprint": '', - "private": False, - 'sign_used': False, - 'refreshed_at': 0, - "expiry_date": 0, - "address": 'some@key', - 'encr_used': False, - 'last_audited_at': 0, - 'key_data': '', - 'length': 0, - 'key_id': '', - 'validation': 'Weak_Chain', - 'type': 'OpenPGPKey', + u'address': u'some@key', + u'encr_used': False, + u'fingerprint': u'', + u'last_audited_at': 0, + u'private': False, + u'sign_used': False, + u'tags': [u'keymanager-active'], + u'type': u'OpenPGPKey-active', + u'validation': u'Weak_Chain', + u'version': 1, } def assert_response(_): diff --git a/service/test/unit/resources/test_logout_resources.py b/service/test/unit/resources/test_logout_resources.py index 6246eeb9..312d2ba4 100644 --- a/service/test/unit/resources/test_logout_resources.py +++ b/service/test/unit/resources/test_logout_resources.py @@ -1,6 +1,6 @@ -from mock import patch -from mockito import mock, verify +from mock import patch, MagicMock from twisted.trial import unittest +from twisted.internet import defer from twisted.web.error import UnsupportedMethod from twisted.web.test.requesthelper import DummyRequest @@ -10,8 +10,9 @@ from test.unit.resources import DummySite class TestLogoutResource(unittest.TestCase): def setUp(self): - self.services_factory = mock() + self.services_factory = MagicMock() self.resource = LogoutResource(self.services_factory) + self.services_factory.log_out_user.return_value = defer.succeed(None) self.web = DummySite(self.resource) @patch('twisted.web.util.redirectTo') @@ -19,14 +20,16 @@ class TestLogoutResource(unittest.TestCase): request = DummyRequest(['/logout']) request.method = 'POST' - mock_redirect.return_value = 'haha' + session = self.resource.get_session(request) + session.expire = MagicMock() + mock_redirect.return_value = 'some redirect response' d = self.web.get(request) def expire_session_and_redirect(_): session = self.resource.get_session(request) - self.assertFalse(session.is_logged_in()) - verify(self.services_factory).log_out_user(session.user_uuid) + self.services_factory.log_out_user.assert_called_once_with(session.user_uuid) + session.expire.assert_called_once_with() mock_redirect.assert_called_once_with('/login', request) d.addCallback(expire_session_and_redirect) diff --git a/web-ui/app/images/logo.svg b/web-ui/app/images/logo.svg new file mode 100644 index 00000000..6c2d8989 --- /dev/null +++ b/web-ui/app/images/logo.svg @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="30.4 627.3 612 171.1" enable-background="new 30.4 627.3 612 171.1" xml:space="preserve"> +<g> + <path fill="#3E3B38" d="M30.4,669.9v85.8l75.5,42.7l75.5-42.7v-85.8l-75.5-42.7L30.4,669.9z M102.7,767.4l-44-24.3v-52.6l44,25 + V767.4z M152.5,743l-43.4,24.3v-51.8l43.4-25.4V743z M152.5,683.1l-46.7,27.8l-47.2-27.8l47.2-25.4L152.5,683.1z"/> + <path fill="#3E3B38" d="M233.8,678.3h-24v71.2h16.2v-26.5h7.8c14,0,24.3-8,24.3-22.9C258.1,685.6,247.6,678.3,233.8,678.3z + M230.6,710.2h-4.6v-18.8h4.6c6.5,0,12.5,2.2,12.5,9.5C243,708.1,237.1,710.2,230.6,710.2z"/> + <rect x="263.5" y="678.3" fill="#3E3B38" width="16.2" height="71.2"/> + <polygon fill="#3E3B38" points="350.4,678.3 330.1,678.3 316.9,697.7 303.7,678.3 284.6,678.3 307,711.1 282.6,749.5 302.9,749.5 + 316.9,725.3 331,749.5 352.1,749.5 326.9,711.1 "/> + <polygon fill="#3E3B38" points="354.7,749.5 395.5,749.5 395.5,735.2 370.9,735.2 370.9,721 394.4,721 394.4,706.6 370.9,706.6 + 370.9,692.5 395.5,692.5 395.5,678.3 354.7,678.3 "/> + <path fill="#3E3B38" d="M456.1,678.3l-22.9,57h-15.9v-57h-16.2v71.2h26.5h14.3h3.2l5.4-14.3h27l5.4,14.3h17.5l-28.9-71.2H456.1z + M455.7,721l7.8-20.7h0.2l7.8,20.7H455.7z"/> + <polygon fill="#3E3B38" points="486.4,692.5 503.4,692.5 503.4,749.5 519.6,749.5 519.6,692.5 536.6,692.5 536.6,678.3 + 486.4,678.3 "/> + <polygon fill="#3E3B38" points="542,749.5 582.8,749.5 582.8,735.2 558.4,735.2 558.4,721 581.9,721 581.9,706.6 558.4,706.6 + 558.4,692.5 582.8,692.5 582.8,678.3 542,678.3 "/> + <path fill="#3E3B38" d="M606.5,678.3h-17.9v71.2h17.9c19.7,0,35.9-14.9,35.9-35.6C642.4,693.1,625.9,678.3,606.5,678.3z M607,735 + h-2.4v-42.1h2.4c12.1,0,20.3,9.1,20.3,21.1C627.3,725.8,619.1,735,607,735z"/> +</g> +<polygon id="clock1" fill="#3E3B38" points="105.8,657.8 105.8,628 105.8,627.3 181.4,669.9 152.5,683.1 "/> +<polygon id="clock2" fill="#3E3B38" points="152.5,683.1 181.4,669.9 181.4,755.7 152.5,743 "/> +<polygon id="clock3" fill="#3E3B38" points="105.9,798.3 105.9,769 152.5,743 181.4,755.7 "/> +<polygon id="clock4" fill="#3E3B38" points="58.7,743.1 105.9,769 105.9,798.3 30.4,755.7 "/> +<polygon id="clock5" fill="#3E3B38" points="30.4,669.9 58.6,683.1 58.7,743.1 30.4,755.7 "/> +<polygon id="clock6" fill="#3E3B38" points="105.8,628 105.8,657.8 58.6,683.1 30.4,669.9 105.8,627.3 "/> +</svg> diff --git a/web-ui/app/js/mail_view/ui/mail_view.js b/web-ui/app/js/mail_view/ui/mail_view.js index 8465b45a..dfc57585 100644 --- a/web-ui/app/js/mail_view/ui/mail_view.js +++ b/web-ui/app/js/mail_view/ui/mail_view.js @@ -76,6 +76,7 @@ define( this.trigger(document, events.search.highlightResults, {where: '.bodyArea'}); this.trigger(document, events.search.highlightResults, {where: '.subjectArea'}); this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'}); + this.trigger(document, events.ui.replyBox.showReplyContainer); this.attachTagCompletion(this.attr.mail); diff --git a/web-ui/app/js/mail_view/ui/reply_section.js b/web-ui/app/js/mail_view/ui/reply_section.js index 46dfe863..cbe64205 100644 --- a/web-ui/app/js/mail_view/ui/reply_section.js +++ b/web-ui/app/js/mail_view/ui/reply_section.js @@ -36,7 +36,8 @@ define( replyAllButton: '#reply-all-button', forwardButton: '#forward-button', replyBox: '#reply-box', - replyType: 'reply' + replyType: 'reply', + replyContainer: '.reply-container' }); this.showReply = function() { @@ -64,9 +65,7 @@ define( this.checkForDraftReply = function() { this.render(); - this.select('replyButton').hide(); - this.select('replyAllButton').hide(); - this.select('forwardButton').hide(); + this.hideContainer(); this.trigger(document, events.mail.draftReply.want, {ident: this.attr.ident}); }; @@ -76,11 +75,13 @@ define( }; 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 }); @@ -89,6 +90,14 @@ define( } }; + 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(); @@ -96,6 +105,7 @@ define( }; this.showButtons = function () { + this.showContainer(); this.select('replyBox').empty(); this.select('replyButton').show(); this.select('replyAllButton').show(); @@ -109,7 +119,7 @@ define( this.on(this, events.mail.here, this.showReplyComposeBox); this.on(document, events.dispatchers.rightPane.clear, this.teardown); - this.on(document, events.mail.draftReply.notFound, this.showButtons); + this.on(document, events.ui.replyBox.showReplyContainer, this.showContainer); this.on(document, events.mail.draftReply.here, this.showDraftReply); this.checkForDraftReply(); diff --git a/web-ui/app/js/page/events.js b/web-ui/app/js/page/events.js index 1ec27c46..7a0dbf9d 100644 --- a/web-ui/app/js/page/events.js +++ b/web-ui/app/js/page/events.js @@ -79,7 +79,8 @@ define(function () { }, replyBox: { showReply: 'ui:replyBox:showReply', - showReplyAll: 'ui:replyBox:showReplyAll' + showReplyAll: 'ui:replyBox:showReplyAll', + showReplyContainer: 'ui:replyBox:showReplyContainer', }, recipients: { entered: 'ui:recipients:entered', diff --git a/web-ui/app/scss/_mixins.scss b/web-ui/app/scss/_mixins.scss index a623366d..5bb84105 100644 --- a/web-ui/app/scss/_mixins.scss +++ b/web-ui/app/scss/_mixins.scss @@ -88,6 +88,7 @@ } ul.tags { + margin-bottom: 0; li { font-size: 0.6rem; background-color: lighten($action_buttons, 12); diff --git a/web-ui/app/scss/_read.scss b/web-ui/app/scss/_read.scss index 7943d584..2c079408 100644 --- a/web-ui/app/scss/_read.scss +++ b/web-ui/app/scss/_read.scss @@ -65,7 +65,7 @@ } .bodyArea { - padding: 15px 30px 0 30px; + padding: 10px 30px 0 30px; } .attachmentsAreaWrap { diff --git a/web-ui/app/scss/_security.scss b/web-ui/app/scss/_security.scss index 8e9a6b5d..ff36cb3e 100644 --- a/web-ui/app/scss/_security.scss +++ b/web-ui/app/scss/_security.scss @@ -3,10 +3,11 @@ clear: both; span { display: inline-block; - padding: 2px 5px; + padding: 2px 6px; white-space: nowrap; background: $success; color: $white; + border-radius: 12px; &:before { font-family: FontAwesome; } diff --git a/web-ui/test/spec/mail_view/ui/mail_view.spec.js b/web-ui/test/spec/mail_view/ui/mail_view.spec.js index ae874621..9ed56023 100644 --- a/web-ui/test/spec/mail_view/ui/mail_view.spec.js +++ b/web-ui/test/spec/mail_view/ui/mail_view.spec.js @@ -29,6 +29,11 @@ describeComponent('mail_view/ui/mail_view', function () { expect(openNoMessageSelectedEvent).toHaveBeenTriggeredOn(document); }); + it('should open reply container', function () { + var showContainerEvent = spyOnEvent(document, Pixelated.events.ui.replyBox.showReplyContainer); + this.component.displayMail({}, testData); + expect(showContainerEvent).toHaveBeenTriggeredOn(document); + }); it('removes the tag from the mail when the tag label is clicked', function() { var updateSpy = spyOnEvent(document, Pixelated.events.mail.tags.update); diff --git a/web-ui/test/spec/mail_view/ui/reply_section.spec.js b/web-ui/test/spec/mail_view/ui/reply_section.spec.js index 9cdf7405..00709684 100644 --- a/web-ui/test/spec/mail_view/ui/reply_section.spec.js +++ b/web-ui/test/spec/mail_view/ui/reply_section.spec.js @@ -5,6 +5,18 @@ describeComponent('mail_view/ui/reply_section', function () { this.setupComponent(); }); + describe('show/hide reply container', function () { + it('should hide reply container until mail data is loaded', function () { + this.component.checkForDraftReply(); + expect(this.component.select('replyContainer')).toBeHidden(); + }); + + it('should show reply container when mail data is loaded', function () { + this.component.trigger(document, Pixelated.events.ui.replyBox.showReplyContainer); + expect(this.component.select('replyContainer')).not.toBeHidden(); + }); + }); + describe('clicking reply buttons', function() { var mailWantEvent, expectEventData; @@ -45,6 +57,7 @@ describeComponent('mail_view/ui/reply_section', function () { this.component.attr.replyType = 'reply'; this.component.trigger(this.component, Pixelated.events.mail.here, { mail: mailData }); + expect(this.component.select('replyContainer')).not.toBeHidden(); expect(ReplyBox.attachTo).toHaveBeenCalledWith(jasmine.any(Object), { mail: mailData, replyType: 'reply' @@ -55,6 +68,7 @@ describeComponent('mail_view/ui/reply_section', function () { this.component.attr.replyType = 'replyall'; this.component.trigger(this.component, Pixelated.events.mail.here, { mail: mailData }); + expect(this.component.select('replyContainer')).not.toBeHidden(); expect(ReplyBox.attachTo).toHaveBeenCalledWith(jasmine.any(Object), { mail: mailData, replyType: 'replyall' @@ -65,6 +79,7 @@ describeComponent('mail_view/ui/reply_section', function () { this.component.attr.replyType = 'forward'; this.component.trigger(this.component, Pixelated.events.mail.here, { mail: mailData }); + expect(this.component.select('replyContainer')).not.toBeHidden(); expect(ForwardBox.attachTo).toHaveBeenCalledWith(jasmine.any(Object), { mail: mailData }); @@ -87,6 +102,7 @@ describeComponent('mail_view/ui/reply_section', function () { $(document).trigger(Pixelated.events.ui.composeBox.trashReply); + expect(this.component.select('replyContainer')).not.toBeHidden(); expect(this.component.select('replyButton')).not.toBeHidden(); expect(this.component.select('replyAllButton')).not.toBeHidden(); expect(this.component.select('forwardButton')).not.toBeHidden(); |