diff options
71 files changed, 2802 insertions, 717 deletions
diff --git a/Vagrantfile b/Vagrantfile index ba925079..828382b9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -30,5 +30,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.provider "virtualbox" do |v, override| v.memory = 1024 override.vm.network :forwarded_port, guest: 3333, host: 3333 # do NOT add host_ip in this line. It is not necessary + override.vm.network :forwarded_port, guest: 8089, host: 8089 end end diff --git a/osx_setup.sh b/osx_setup.sh index a8a37833..d042d045 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,8 @@ 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) } function install_rbenv { @@ -30,13 +60,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/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 86435d89..109dc08e 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -20,6 +20,7 @@ from string import Template from pixelated.resources import BaseResource, UnAuthorizedResource from pixelated.resources.attachments_resource import AttachmentsResource +from pixelated.resources.sandbox_resource import SandboxResource from pixelated.resources.contacts_resource import ContactsResource from pixelated.resources.features_resource import FeaturesResource from pixelated.resources.feedback_resource import FeedbackResource @@ -75,6 +76,7 @@ class RootResource(BaseResource): return csrf_input and csrf_input == xsrf_token def initialize(self, portal=None, disclaimer_banner=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)) diff --git a/service/pixelated/resources/sandbox_resource.py b/service/pixelated/resources/sandbox_resource.py new file mode 100644 index 00000000..28e8c9be --- /dev/null +++ b/service/pixelated/resources/sandbox_resource.py @@ -0,0 +1,34 @@ +# +# Copyright (c) 2016 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + +from twisted.web.static import File + + +class SandboxResource(File): + CSP_HEADER_VALUES = "sandbox allow-popups allow-scripts;" \ + "default-src 'self';" \ + "style-src *;" \ + "script-src *;" \ + "font-src *;" \ + "img-src *;" \ + "object-src 'none';" \ + "connect-src 'none';" + + def render_GET(self, request): + request.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES) + request.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES) + request.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES) + return super(SandboxResource, self).render_GET(request) diff --git a/service/requirements.txt b/service/requirements.txt index 1966f09a..b74b7f94 100644 --- a/service/requirements.txt +++ b/service/requirements.txt @@ -4,15 +4,15 @@ https://launchpad.net/dirspec/stable-13-10/13.10/+download/dirspec-13.10.tar.gz --allow-external dirspec --allow-unverified dirspec https://launchpad.net/u1db/stable-13-10/13.10/+download/u1db-13.10.tar.bz2 pyasn1==0.1.8 -requests==2.0.0 +requests==2.9.1 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/functional/features/steps/attachments.py b/service/test/functional/features/steps/attachments.py index 76e42177..8fa032df 100644 --- a/service/test/functional/features/steps/attachments.py +++ b/service/test/functional/features/steps/attachments.py @@ -51,7 +51,7 @@ def load_mail_into_soledad(context, mail): @then(u'I see the mail has an attachment') def step_impl(context): - attachments_list = find_elements_by_css_selector(context, '.attachmentsArea li') + attachments_list = find_elements_by_css_selector(context, '.mail-read-view__attachments-item') assert len(attachments_list) == 1 diff --git a/service/test/functional/features/steps/mail_view.py b/service/test/functional/features/steps/mail_view.py index 82fc28af..565031b5 100644 --- a/service/test/functional/features/steps/mail_view.py +++ b/service/test/functional/features/steps/mail_view.py @@ -19,14 +19,17 @@ from common import * @then('I see that the subject reads \'{subject}\'') def impl(context, subject): - e = find_element_by_css_selector(context, '#mail-view .subject') + e = find_element_by_css_selector(context, '#mail-view .mail-read-view__header-subject') assert e.text == subject @then('I see that the body reads \'{expected_body}\'') def impl(context, expected_body): - e = find_element_by_css_selector(context, '#mail-view .bodyArea') + find_element_by_css_selector(context, '#read-sandbox') + context.browser.switch_to_frame('read-sandbox') + e = find_element_by_css_selector(context, 'body') assert e.text == expected_body + context.browser.switch_to_default_content() @then('that email has the \'{tag}\' tag') @@ -97,13 +100,13 @@ def impl(context): @when('I choose to trash') def impl(context): context.browser.execute_script("$('button#view-more-actions').click()") - click_button(context, 'Delete this message', 'span') + click_button(context, 'Delete this message', 'li') @then('I see the mail has a cc and a bcc recipient') def impl(context): - cc = find_element_by_css_selector(context, '.msg-header .cc') - bcc = find_element_by_css_selector(context, '.msg-header .bcc') + cc = find_element_by_css_selector(context, '.mail-read-view__header-recipients .cc') + bcc = find_element_by_css_selector(context, '.mail-read-view__header-recipients .bcc') assert cc is not None assert bcc is not None diff --git a/service/test/load/locustfile.py b/service/test/load/locustfile.py index 68e39433..0c2ed518 100644 --- a/service/test/load/locustfile.py +++ b/service/test/load/locustfile.py @@ -1,6 +1,5 @@ import os import json -import time from random import randint from leap.auth import SRPAuth @@ -9,9 +8,13 @@ from locust import HttpLocust, TaskSet, task from pixelated.resources.login_resource import LoginResource LEAP_PROVIDER = os.environ.get('LEAP_PROVIDER', 'dev.pixelated-project.org') -LEAP_SERVER_HOST = os.environ.get('LEAP_SERVER_HOST', 'https://api.%s:4430' % LEAP_PROVIDER) -LEAP_VERIFY_CERTIFICATE = os.environ.get('LEAP_VERIFY_CERTIFICATE', '~/.leap/ca.crt') -MAX_NUMBER_USER = os.environ.get('MAX_NUMBER_USER', 10000) +LEAP_SERVER_HOST = os.environ.get( + 'LEAP_SERVER_HOST', + 'https://api.%s:4430' % LEAP_PROVIDER) +LEAP_VERIFY_CERTIFICATE = os.environ.get( + 'LEAP_VERIFY_CERTIFICATE', + '~/.leap/ca.crt') +MAX_NUMBER_USER = os.environ.get('MAX_NUMBER_USER', 100) INVITES_FILENAME = os.environ.get('INVITES_FILENAME', '/tmp/invite_codes.txt') INVITES_ENABLED = os.environ.get('INVITES_ENABLED', 'true') == 'true' @@ -23,53 +26,94 @@ def load_invite_from_number(number): class UserBehavior(TaskSet): + def __init__(self, *args, **kwargs): + super(UserBehavior, self).__init__(*args, **kwargs) + self.cookies = {} + def on_start(self): - """ on_start is called when a Locust start before any task is scheduled """ self.login() def _get_or_create_user(self, number): - srp_auth = SRPAuth(LEAP_SERVER_HOST, os.path.expanduser(LEAP_VERIFY_CERTIFICATE)) + srp_auth = SRPAuth( + LEAP_SERVER_HOST, + os.path.expanduser(LEAP_VERIFY_CERTIFICATE)) username, password = ('loadtest%d' % number), ('password_%d' % number) try: srp_auth.authenticate(username, password) except SRPAuthenticationError: - invite_code = load_invite_from_number(number) if INVITES_ENABLED else None + invite_code = None + if INVITES_ENABLED: + invite_code = load_invite_from_number(number) + srp_auth.register(username, password, invite_code) return username, password def login(self): number = randint(1, int(MAX_NUMBER_USER)) username, password = self._get_or_create_user(number) - self.client.post("/%s" % LoginResource.BASE_URL, {"username": username, "password": password}) + response = self.client.post( + "/%s" % LoginResource.BASE_URL, + {"username": username, "password": password}, + verify=False) + self.cookies.update(response.cookies.get_dict()) + resp = self.client.get("/", verify=False) + self.cookies.update(resp.cookies.get_dict()) self.username = username - time.sleep(5) @task(1) def index(self): - self.client.get("/") + self.client.get("/", verify=False) @task(2) def mail_box(self): - self.client.get("/mails?q=tag:'inbox'&p=1&w=25") + self.client.get("/mails?q=tag:'inbox'&p=1&w=25", verify=False) @task(3) def send_mail(self): - payload = {"tags": ["drafts"], "body": "some text lorem ipsum", "attachments": [], "ident": "", - "header": {"to": ["%s@%s" % (self.username, LEAP_PROVIDER)], "cc": [], "bcc": [], "subject": "load testing"}} - with self.client.post('/mails', json=payload, catch_response=True) as email_response: + payload = { + "tags": ["drafts"], + "body": "some text lorem ipsum", + "attachments": [], + "ident": "", + "header": { + "to": ["%s@%s" % (self.username, LEAP_PROVIDER)], + "cc": [], + "bcc": [], + "subject": "load testing"}} + + self.cookies.update( + self.client.get("/", verify=False).cookies.get_dict()) + print(self.cookies) + with self.client.post( + '/mails', + json=payload, + catch_response=True, + cookies=self.cookies, + headers={ + 'X-Requested-With': 'XMLHttpRequest', + 'X-XSRF-TOKEN': self.cookies['XSRF-TOKEN']}) as email_response: if email_response.status_code == 201: email_id = json.loads(email_response.content)['ident'] print email_id self.delete_mail(email_id) else: - email_response.failure('Error: email not Sent, status code: %s' % email_response.status_code) + email_response.failure( + 'Error: email not Sent, status code: %s' % ( + email_response.status_code)) def delete_mail(self, ident): payload = {"idents": [ident]} - self.client.post('/mails/delete', json=payload) + self.client.post( + '/mails/delete', + json=payload, + cookies=self.cookies, + verify=False, + headers={ + 'X-Requested-With': 'XMLHttpRequest', + 'X-XSRF-TOKEN': self.cookies['XSRF-TOKEN']}) class WebsiteUser(HttpLocust): task_set = UserBehavior - min_wait = 3000 + min_wait = 5000 max_wait = 15000 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/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/service/test/unit/resources/test_sandbox_resource.py b/service/test/unit/resources/test_sandbox_resource.py new file mode 100644 index 00000000..3db43e12 --- /dev/null +++ b/service/test/unit/resources/test_sandbox_resource.py @@ -0,0 +1,38 @@ +import os +import unittest + +from twisted.internet import defer +from twisted.web.test.requesthelper import DummyRequest + +from pixelated.resources.sandbox_resource import SandboxResource +from test.unit.resources import DummySite + + +class TestSandBoxResource(unittest.TestCase): + def setUp(self): + static_folder = os.path.dirname(os.path.abspath(__file__)) + self.resource = SandboxResource(static_folder) + self.resource.isLeaf = True + self.web = DummySite(self.resource) + + @defer.inlineCallbacks + def test_render_GET_should_set_sandbox_csp_header(self): + request = DummyRequest(['/sandbox']) + request.method = 'GET' + request.isSecure = lambda: True + request.redirect = lambda _: 'irrelevant' + + expected_csp_headers = "sandbox allow-popups allow-scripts;" \ + "default-src 'self';" \ + "style-src *;" \ + "script-src *;" \ + "font-src *;" \ + "img-src *;" \ + "object-src 'none';" \ + "connect-src 'none';" + + yield self.web.get(request) + + self.assertEquals(expected_csp_headers, request.outgoingHeaders.get('X-Content-Security-Policy'.lower())) + self.assertEquals(expected_csp_headers, request.outgoingHeaders.get('Content-Security-Policy'.lower())) + self.assertEquals(expected_csp_headers, request.outgoingHeaders.get('X-Webkit-CSP'.lower())) diff --git a/service/test_requirements.txt b/service/test_requirements.txt index 3d4c73cb..52278140 100644 --- a/service/test_requirements.txt +++ b/service/test_requirements.txt @@ -10,4 +10,5 @@ tempdir==0.6 coverage crochet==1.4.0 poster==0.8.1 -locustio==0.7.3
\ No newline at end of file +locustio==0.7.3 +Twisted==15.5.0 diff --git a/web-ui/app/index.html b/web-ui/app/index.html index c8a1abb5..4b6a81a0 100644 --- a/web-ui/app/index.html +++ b/web-ui/app/index.html @@ -9,8 +9,7 @@ <meta name="viewport" content="width=device-width"> <link href="assets/bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="assets/bower_components/jquery-file-upload/css/jquery.fileupload.css" rel="stylesheet" type="text/css"> -<link href="assets/css/opensans.css" rel="stylesheet" type="text/css"> -<link rel="stylesheet" href="assets/css/main.css"> +<link rel="stylesheet" href="assets/css/style.css"> </head> <body> @@ -62,7 +61,7 @@ </div> <div class="off-canvas-wrap content" data-offcanvas> - <header id="main" > + <header class="message-panel-container" > <div id="user-alerts" class="message-panel"></div> </header> @@ -106,6 +105,7 @@ <script src="assets/bower_components/foundation/js/foundation/foundation.reveal.js" ></script> <script src="assets/bower_components/foundation/js/foundation/foundation.offcanvas.js"></script> <script src="assets/js/foundation/initialize_foundation.js"></script> +<script src="assets/bower_components/iframe-resizer/js/iframeResizer.min.js"></script> <script src="assets/bower_components/requirejs/require.js" data-main="assets/js/main.js"></script> <!--usemin_end--> diff --git a/web-ui/app/js/helpers/sanitizer.js b/web-ui/app/js/helpers/sanitizer.js index eea1f0f7..443e8602 100644 --- a/web-ui/app/js/helpers/sanitizer.js +++ b/web-ui/app/js/helpers/sanitizer.js @@ -23,6 +23,16 @@ define(['DOMPurify', 'he'], function (DOMPurify, he) { */ var sanitizer = {}; + sanitizer.whitelist = [{ + // highlight tag open + pre: '<em class="search-highlight">', + post: '<em class="search-highlight">' + }, { + // highlight tag close + pre: '</em>', + post: '</em>' + }]; + /** * Adds html line breaks to a plaintext with line breaks (incl carriage return) * @@ -55,16 +65,24 @@ define(['DOMPurify', 'he'], function (DOMPurify, he) { }; /** - * 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 - */ + * 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) { - return he.encode(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; }; /** diff --git a/web-ui/app/js/mail_list/ui/mail_list.js b/web-ui/app/js/mail_list/ui/mail_list.js index 18d36049..0f6c4fb5 100644 --- a/web-ui/app/js/mail_list/ui/mail_list.js +++ b/web-ui/app/js/mail_list/ui/mail_list.js @@ -81,7 +81,6 @@ define( 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: '.bodyArea'}); this.trigger(document, events.search.highlightResults, {where: '.subjectArea'}); this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'}); }; 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..d952fed7 100644 --- a/web-ui/app/js/mail_view/ui/mail_view.js +++ b/web-ui/app/js/mail_view/ui/mail_view.js @@ -71,11 +71,55 @@ define( attachments: attachments })); - this.$node.find('.bodyArea').html(viewHelpers.formatMailBody(data.mail)); + var $iframe = $("#read-sandbox"); + var iframe = $iframe[0]; + + var content = viewHelpers.formatMailBody(data.mail); + + iframe.onload = function() { + // use iframe-resizer to dynamically adapt iframe size to its content + var config = { + resizedCallback: scaleToFit, + checkOrigin: false + }; + $iframe.iFrameResize(config); + + // 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 + var mail = data.mail; + 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 + }); + } + + iframe.contentWindow.postMessage({ + html: content + }, '*'); + }; + + - 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); @@ -213,9 +257,17 @@ define( 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.here, this.displayMail); 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); 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 1f48173d..68a6aad1 100644 --- a/web-ui/app/js/page/events.js +++ b/web-ui/app/js/page/events.js @@ -81,7 +81,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', @@ -122,6 +123,8 @@ define(function () { mail: { here: 'mail:here', want: 'mail:want', + display: 'mail:display', + highlightMailContent: 'mail:highlightMailContent', send: 'mail:send', send_failed: 'mail:send_failed', sent: 'mail:sent', diff --git a/web-ui/app/js/sandbox.js b/web-ui/app/js/sandbox.js new file mode 100644 index 00000000..f9e708d6 --- /dev/null +++ b/web-ui/app/js/sandbox.js @@ -0,0 +1,9 @@ +(function () { + 'use strict'; + + window.onmessage = function (e) { + if (e.data.html) { + document.body.innerHTML = e.data.html; + } + }; +})(); diff --git a/web-ui/app/js/search/results_highlighter.js b/web-ui/app/js/search/results_highlighter.js index 9e3ba167..831be0cd 100644 --- a/web-ui/app/js/search/results_highlighter.js +++ b/web-ui/app/js/search/results_highlighter.js @@ -40,6 +40,7 @@ define( 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' @@ -57,12 +58,40 @@ define( }); }; + this.highlightString = function (string) { + _.each(this.attr.keywords, function (keyword) { + keyword = escapeRegExp(keyword); + var regex = new RegExp('(' + keyword + ')', 'ig'); + string = string.replace(regex, '<em class="search-highlight">$1</em>'); + }); + return string; + }; + + /* + * Alter data.mail.textPlainBody to highlight each of this.attr.keywords + * and pass it back to the mail_view when done + */ + this.highlightMailContent = function(ev, data){ + var mail = data.mail; + mail.textPlainBody = this.highlightString(mail.textPlainBody); + this.trigger(document, events.mail.display, data); + }; + + /* + * Escapes the special charaters used regular expressions that + * would cause problems with strings in the RegExp constructor + */ + function escapeRegExp(string){ + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + this.after('initialize', function () { this.on(document, events.search.perform, this.getKeywordsSearch); this.on(document, events.ui.tag.select, this.clearHighlights); this.on(document, events.search.resetHighlight, this.clearHighlights); this.on(document, events.search.highlightResults, this.highlightResults); + this.on(document, events.mail.highlightMailContent, this.highlightMailContent); }); } }); diff --git a/web-ui/app/js/user_alerts/ui/user_alerts.js b/web-ui/app/js/user_alerts/ui/user_alerts.js index b02762aa..e944a7a5 100644 --- a/web-ui/app/js/user_alerts/ui/user_alerts.js +++ b/web-ui/app/js/user_alerts/ui/user_alerts.js @@ -32,20 +32,26 @@ define( dismissTimeout: 3000 }); - this.render = function (message) { + 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: (data.class || 'success')}}); + this.displayMessage = function(ev, data) { + this.render({ + message: { + content: data.message, + class: 'message-panel__growl--' + (data.class || 'success') + } + }); }; - this.after('initialize', function () { + this.after('initialize', function() { this.on(document, events.ui.userAlerts.displayMessage, this.displayMessage); }); } } ); + diff --git a/web-ui/app/sandbox.html b/web-ui/app/sandbox.html new file mode 100644 index 00000000..13a86f25 --- /dev/null +++ b/web-ui/app/sandbox.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <link href="css/opensans.css" rel="stylesheet" type="text/css"> + <link href="css/sandbox.css" rel="stylesheet" type="text/css"> + + <!--usemin_start--> + <script src="js/sandbox.js"></script> + <script src="bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script> + <!--usemin_end--> +</head> + +<body></body> + +</html> diff --git a/web-ui/app/scss/_alerts.scss b/web-ui/app/scss/_alerts.scss deleted file mode 100644 index 8d7396fc..00000000 --- a/web-ui/app/scss/_alerts.scss +++ /dev/null @@ -1,25 +0,0 @@ -.message-panel { - width: 100%; - margin: 10px auto; - position: fixed; - z-index: 10000; - text-align: center; - span{ - 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%)); - } - } -} - -@include logo; diff --git a/web-ui/app/scss/_compose.scss b/web-ui/app/scss/_compose.scss deleted file mode 100644 index 32d11c3d..00000000 --- a/web-ui/app/scss/_compose.scss +++ /dev/null @@ -1,185 +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; - @include btn-transition; - &:hover { - background: lighten($action_buttons, 10%); - cursor: pointer; - } - } - } -} - -// COMPOSE PANE -#compose-box, #draft-box, #reply-box, #feedback-box { - div.floatlabel { - position: relative; - } - margin: 5px 0 50px 30px; - padding: 0; - .input-container { - padding: 1px; - } - label, span { - color: $recipients_font_color; - padding: 0.5rem; - cursor: text; - 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; - } - - #attachment-upload-item { - display: none; - - .progress { - width: 0%; - position: absolute; - right: 0; - left: 0; - top: 0; - bottom: 0; - min-height: 100%; - - .progress-bar { - height: 100%; - background-color: rgba($light_blue, 0.3); - } - - } - } - - .attachmentsAreaWrap { - padding: 0; - - .attachmentsArea { - padding: 0; - border-top: 0; - - #upload-error { - color: $error; - margin-bottom: 20px; - - .close-icon { - font-size: 1.0rem; - cursor: pointer; - } - - span, a { - color: $error; - font-size: 0.9rem; - } - - a { - text-decoration: underline; - padding: 5px; - } - } - - } - } - - @include recipients; -} - -#reply-box { - @include recipients; -} diff --git a/web-ui/app/scss/_mascot.scss b/web-ui/app/scss/_mascot.scss deleted file mode 100644 index 74279063..00000000 --- a/web-ui/app/scss/_mascot.scss +++ /dev/null @@ -1,47 +0,0 @@ -#no-message-selected-pane { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100vh; - - z-index: -100; - background: $contrast; - padding: 30px; - vertical-align:middle; - text-align:center; - -webkit-transform: translate3d(0, 0, 0); - &:before{ - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - margin-right: -0.25em; - } - .scene{ - display:inline-block; - vertical-align:middle; - } - - .text{ - color:$medium_dark_grey; - margin-bottom: 40px; - } -} - -#no-mails-available-pane { - text-align: center; - line-height: 100vh; - margin-top: -130px; - - .scene{ - display:inline-block; - vertical-align:middle; - } - - .text{ - color:$medium_dark_grey; - margin-bottom: 40px; - } - -} diff --git a/web-ui/app/scss/_mixins.scss b/web-ui/app/scss/_mixins.scss index 6d0bb1a3..13f90685 100644 --- a/web-ui/app/scss/_mixins.scss +++ b/web-ui/app/scss/_mixins.scss @@ -1,4 +1,4 @@ -@import 'colors'; +@import 'base/colors'; // SHARED MIXINS @mixin btn-transition { @@ -88,6 +88,7 @@ } ul.tags { + margin-bottom: 0; li { font-size: 0.6rem; background-color: lighten($action_buttons, 12); @@ -161,7 +162,6 @@ } } - @mixin recipients { .recipients-area { @@ -344,3 +344,4 @@ } @include tt-hint; +@include logo; diff --git a/web-ui/app/scss/_others.scss b/web-ui/app/scss/_others.scss new file mode 100644 index 00000000..e73ed33d --- /dev/null +++ b/web-ui/app/scss/_others.scss @@ -0,0 +1,7 @@ +.no-padding { + padding: 0; +} + +.text-right { + text-align: right; +} diff --git a/web-ui/app/scss/_read.scss b/web-ui/app/scss/_read.scss deleted file mode 100644 index 7943d584..00000000 --- a/web-ui/app/scss/_read.scss +++ /dev/null @@ -1,164 +0,0 @@ -/* MAIL PANE */ - -@mixin read-msg { - #mail-view { - .msg-header { - display: flex; - flex-wrap: nowrap; - - top: 0; - z-index: 10; - background-color: white; - font-size: 0.9em; - padding: 0px 0; - margin: 1px 0 0 0; - .recipients { - padding-bottom: 5px; - line-height: 1.5em; - i { - padding: 0 5px; - } - .from { - font-weight: 700; - } - } - .close-mail-button { - position: relative; - float: none; - flex-shrink: 0; - display: inline-block; - vertical-align: top; - height: 27px; - margin-right: 3px; - } - - .full-view-header { - display:inline-block; - padding-top: 5px; - width:95%; - flex-shrink:1; - } - .headline-area { - clear: both; - border-top: 1px solid $lighter_gray; - } - } - h3 { - margin-bottom: 0; - } - .tagsArea { - clear: both; - margin: 0 0 10px; - @include tags; - ul li { - &.tag:hover { - &:before { - content: "click to remove"; - text-transform: lowercase; - font-size: 0.5rem; - @include tooltip(18px, 8px); - } - } - } - } - } -} - -.bodyArea { - padding: 15px 30px 0 30px; -} - -.attachmentsAreaWrap { - padding: 0 30px; - - .attachmentsArea { - border-top: 1px solid $lighter_gray; - padding: 10px 0 0; - - li { - position: relative; - display: block; - border: 1px solid $border_light_grey; - border-radius: 2px; - background-color: $background_light_grey; - margin-bottom: 8px; - - a { - color: $attachment_text; - display: block; - text-decoration: none; - line-height: inherit; - padding: 4px 5px; - - &:hover, &:focus { - i.download-icon { - color: lighten($attachment_icon, 15); - } - - color: $attachment_icon; - outline: none; - } - - i.download-icon { - position: absolute; - color: $attachment_icon; - right: 0; - padding: 7px 10px; - } - - i.remove-icon { - float: right; - padding-right: 5px; - padding-top: 7px; - color: $medium_light_grey; - } - } - } - } -} - -#mail-actions { - text-align: right; - padding: 10px 0; - button { - display: inline-block; - display: inline; - line-height: 2em; - border: 1px solid $lighter_gray; - &#reply-button-top { - @include border-right-radius(0); - padding: 0 20px; - } - &#view-more-actions { - @include border-left-radius(0); - padding: 0 5px; - margin-left: -4px; - } - &:hover { - @include btn-transition; - background: darken($contrast, 5%) - } - } - ul#more-actions { - padding: 5px 0; - width: 170px; - text-align: left; - display: block; - position: absolute; - background: $white; - border: 1px solid $lighter_gray; - right: 0; - top: 40px; - z-index: 10; - li { - span, a { - padding: 5px 10px; - display: block; - &:hover { - cursor: pointer; - background: $contrast; - } - } - } - } -} diff --git a/web-ui/app/scss/_reply.scss b/web-ui/app/scss/_reply.scss index 76689d16..622bdd5a 100644 --- a/web-ui/app/scss/_reply.scss +++ b/web-ui/app/scss/_reply.scss @@ -1,6 +1,6 @@ #reply-section { - padding: 0 30px; + padding-left: 30px; .reply-container { margin: 10px 0; padding: 10px; 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/app/scss/_styles.scss b/web-ui/app/scss/_styles.scss index 63f15f6a..98c70c68 100644 --- a/web-ui/app/scss/_styles.scss +++ b/web-ui/app/scss/_styles.scss @@ -528,10 +528,6 @@ section { box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.12); z-index: 2; overflow-y: auto; - @include read-msg; - [id^=fullView-] { - position: relative; - } } } @@ -578,9 +574,11 @@ section { button { border: 1px solid transparent; + i { margin-left: 5px; } + &#trash-button { background: $white; border: 1px solid $medium_light_grey; @@ -591,30 +589,8 @@ button { background: $contrast; } } - &.close-mail-button { - background: transparent; - color: $medium_light_grey; - float: right; - &:hover { - color: darken($medium_light_grey, 10%); - } - } - &.close-mail-button { - position: absolute; - left: 0; - top: 0; - margin: 0; - padding: 3px 6px 5px; - background: $lighter_gray; - opacity: 0.7; - @include border-radius(0); - &:hover { - opacity: 1; - } - i { - margin: 0; - } - } + + &.no-style { background: transparent; color: $medium_light_grey; diff --git a/web-ui/app/scss/_colors.scss b/web-ui/app/scss/base/_colors.scss index de5f76b3..de5f76b3 100644 --- a/web-ui/app/scss/_colors.scss +++ b/web-ui/app/scss/base/_colors.scss diff --git a/web-ui/app/scss/opensans.scss b/web-ui/app/scss/base/_fonts.scss index ada6a025..ada6a025 100644 --- a/web-ui/app/scss/opensans.scss +++ b/web-ui/app/scss/base/_fonts.scss diff --git a/web-ui/app/scss/base/_scaffolding.scss b/web-ui/app/scss/base/_scaffolding.scss new file mode 100644 index 00000000..b8b5fa3b --- /dev/null +++ b/web-ui/app/scss/base/_scaffolding.scss @@ -0,0 +1,10 @@ +html { + height: 100% ; +} + +body { + min-height: 100% ; + overflow: hidden; + background: $white; +} + diff --git a/web-ui/app/scss/main.scss b/web-ui/app/scss/main.scss deleted file mode 100644 index b582a5d5..00000000 --- a/web-ui/app/scss/main.scss +++ /dev/null @@ -1,37 +0,0 @@ -@import "compass/css3"; -@import "reset"; -@import "foundation"; -@import "colors"; -@import "mixins"; -@import "alerts"; -@import "read"; -@import "reply"; -@import "compose"; -@import "security"; -@import "mascot"; -@import "styles"; - -html { - height:100%; -} -body { - min-height:100%; - overflow: hidden; - background: $white; -} -header#main { - overflow: hidden; - position: fixed; - top: 0; - width: 100%; - position: relative; - margin-bottom: 0; -} - -.no-padding { - padding: 0; -} - -.text-right { - text-align: right; -} diff --git a/web-ui/app/scss/mixins/_position-helpers.scss b/web-ui/app/scss/mixins/_position-helpers.scss new file mode 100644 index 00000000..254bfc6c --- /dev/null +++ b/web-ui/app/scss/mixins/_position-helpers.scss @@ -0,0 +1,9 @@ +@mixin absolute-center-unknown-height-width() { + margin: auto; + position: absolute; + left: 50%; + top: 50%; + -ms-transform: translate(-50%, -50%); + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} diff --git a/web-ui/app/scss/sandbox.scss b/web-ui/app/scss/sandbox.scss new file mode 100644 index 00000000..3cb4c441 --- /dev/null +++ b/web-ui/app/scss/sandbox.scss @@ -0,0 +1,20 @@ +$search-highlight: #FFEF29; + +body { + font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; + font-size: 13px; + line-height: 1.2em; + background: white; + color: #333; + padding: 0; + margin: 0; + font-weight: normal; + -webkit-font-smoothing: antialiased; + font-style: normal; + box-sizing: border-box; + word-wrap: break-word; +} + +.search-highlight { + background-color: $search-highlight; +} diff --git a/web-ui/app/scss/style.scss b/web-ui/app/scss/style.scss new file mode 100644 index 00000000..cb905c27 --- /dev/null +++ b/web-ui/app/scss/style.scss @@ -0,0 +1,35 @@ +// vendor stylesheets and resets +@import "vendor/reset"; +@import "vendor/scut"; +@import "compass/css3"; +@import "vendor/foundation"; + +// basic configuration +@import "base/fonts"; +@import "base/colors"; +@import "base/scaffolding"; + +// mixins +@import "mixins/position-helpers"; +@import "mixins"; + +// templates +@import "templates/no-content-placeholder"; + +// 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/compose-view"; +@import "views/compose-button"; + +// misc stuff +@import "others"; + +// TODO +@import "reply"; +@import "security"; +@import "styles"; + diff --git a/web-ui/app/scss/templates/_no-content-placeholder.scss b/web-ui/app/scss/templates/_no-content-placeholder.scss new file mode 100644 index 00000000..c6807011 --- /dev/null +++ b/web-ui/app/scss/templates/_no-content-placeholder.scss @@ -0,0 +1,5 @@ +.no-content-placeholder { + @include absolute-center-unknown-height-width; + + color: $medium_dark_grey; +} diff --git a/web-ui/app/scss/foundation.scss b/web-ui/app/scss/vendor/_foundation.scss index 7918cf26..7918cf26 100644 --- a/web-ui/app/scss/foundation.scss +++ b/web-ui/app/scss/vendor/_foundation.scss diff --git a/web-ui/app/scss/reset.scss b/web-ui/app/scss/vendor/_reset.scss index 55f8d054..55f8d054 100644 --- a/web-ui/app/scss/reset.scss +++ b/web-ui/app/scss/vendor/_reset.scss diff --git a/web-ui/app/scss/vendor/_scut.scss b/web-ui/app/scss/vendor/_scut.scss new file mode 100644 index 00000000..3e16fa65 --- /dev/null +++ b/web-ui/app/scss/vendor/_scut.scss @@ -0,0 +1,1518 @@ +/* +* Scut, a collection of Sass utilities +* to ease and improve our implementations of common style-code patterns. +* v1.3.0 +* Docs at http://davidtheclark.github.io/scut +*/ + +@mixin scut-clearfix { + + &:after { + content: ""; + display: table; + clear: both; + } + +} + +%scut-clearfix { + @include scut-clearfix; +} +@mixin scut-list-unstyled( + $no-margin: true +) { + + list-style-type: none; + padding-left: 0; + + @if $no-margin { + margin-top: 0; + margin-bottom: 0; + } + +} + +%scut-list-unstyled { + @include scut-list-unstyled(); +} +// Depends on `list-unstyled` and `clearfix`. + +@mixin scut-list-floated ( + $space: false, + $dir: left, + $no-margin: true +) { + + @include scut-list-unstyled($no-margin); + @include scut-clearfix; + + & > li { + float: $dir; + } + + @if $space { + & > li + li { + margin-#{$dir}: $space; + } + } + +} + +%scut-list-floated { + @include scut-list-floated; +} + +@function scut-autoOrValue ($val) { + @if $val == a or $val == auto { + @return auto; + } + @else { + @return $val; + } +} + +@mixin scut-coords ( + $coordinates: n n n n +) { + + $top: nth($coordinates, 1); + $right: nth($coordinates, 2); + $bottom: nth($coordinates, 3); + $left: nth($coordinates, 4); + + @if $top != n { + top: scut-autoOrValue($top); + } + @if $right != n { + right: scut-autoOrValue($right); + } + @if $bottom != n { + bottom: scut-autoOrValue($bottom); + } + @if $left != n { + left: scut-autoOrValue($left); + } + +} +@function scut-strip-unit ( + $num +) { + + @return $num / ($num * 0 + 1); + +} +// Depends on `scut-strip-unit`. + +$scut-em-base: 16 !default; + +@function scut-em ( + $pixels, + $base: $scut-em-base +) { + + // $base could be in em or px (no unit = px). + // Adjust accordingly to create a $divisor that + // serves as context for $pixels. + $multiplier: if(unit($base) == em, 16, 1); + $divisor: scut-strip-unit($base) * $multiplier; + + $em-vals: (); + @each $val in $pixels { + $val-in-ems: (scut-strip-unit($val) / $divisor) * 1em; + $em-vals: append($em-vals, $val-in-ems); + } + + @if length($em-vals) == 1 { + // return a single value instead of a list, + // so it can be used in calculations + @return nth($em-vals, 1); + } + @else { + @return $em-vals; + } + +} +// Depends on `scut-strip-unit`. + +$scut-rem-base: 16 !default; + +@function scut-rem ( + $pixels +) { + + $rem-vals: (); + @each $val in $pixels { + $val-in-rems: scut-strip-unit($val) / $scut-rem-base * 1rem; + $rem-vals: append($rem-vals, $val-in-rems); + } + + @if length($rem-vals) == 1 { + // return a single value instead of a list, + // so it can be used in calculations + @return nth($rem-vals, 1); + } + @else { + @return $rem-vals; + } + +} +@mixin scut-border ( + $style, + $sides: n y +) { + + @if length($sides) == 2 { + @if nth($sides, 1) != n { + border-top: $style; + border-bottom: $style; + } + @if nth($sides, 2) != n { + border-left: $style; + border-right: $style; + } + } + + @else if length($sides) == 4 { + @if nth($sides, 1) != n { + border-top: $style; + } + @if nth($sides, 2) != n { + border-right: $style; + } + @if nth($sides, 3) != n { + border-bottom: $style; + } + @if nth($sides, 4) != n { + border-left: $style; + } + } + + @else { + @warn "Scut-border requires a $sides argument of 2 or 4 values." + } + +} +@mixin scut-circle ( + $size, + $color: inherit +) { + + border-radius: 50%; + display: inline-block; + + @if $color == inherit { + // If user wants to inherit the color, + // take advantage of the fact that border + // color defaults to the text color of the element. + border-width: $size / 2; + border-style: solid; + height: 0; + width: 0; + } + @else { + // Otherwise, just use background-color. + background-color: $color; + height: $size; + width: $size; + } + +} +@mixin scut-color-swap ( + $off, + $on, + $duration: 0, + $bg: false +) { + + $transition-properties: null; + $off-is-list: type-of($off) == list; + $on-is-list: type-of($on) == list; + + // If $off IS a list, + // assign color and background-color. + @if $off-is-list { + color: nth($off, 1); + background-color: nth($off, 2); + $transition-properties: background-color, color; + } + + // If $off IS NOT a list and $bg is TRUE, + // assign background-color. + @else if $bg and not($off-is-list) { + background-color: $off; + $transition-properties: background-color; + } + + // If $off IS NOT a list and $bg is FALSE, + // assign color. + @else { + color: $off; + $transition-properties: color; + } + + // Only set-up transition if $duration != 0. + @if $duration != 0 { + transition-property: $transition-properties; + transition-duration: $duration; + } + + &:hover, + &:focus { + + // $on is treated the same as $off, above. + @if $on-is-list { + color: nth($on, 1); + background-color: nth($on, 2); + } + + @else if $bg and not($on-is-list) { + background-color: $on; + } + + @else { + color: $on; + } + } + +} +@mixin scut-hd-bp ( + $ratio: 1.3 +) { + + @media (-o-min-device-pixel-ratio: ($ratio / 1)), + (-webkit-min-device-pixel-ratio: $ratio), + (min-resolution: (round(96 * $ratio) * 1dpi)) { + @content; + } + +} + +@mixin scut-hide-visually { + + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + +} + +%scut-hide-visually { + @include scut-hide-visually; +} +@mixin scut-image-replace { + + text-indent: 102%; + white-space: nowrap; + overflow: hidden; + padding: 0; + +} + +%scut-image-replace { + @include scut-image-replace; +} + +// Depends on scut-rem and scut-strip-unit + +@mixin scut-rem-fallback ( + $pixels, + $property: font-size +) { + + $px-vals: null; + @each $val in $pixels { + $val-in-px: scut-strip-unit($val) * 1px; + $px-vals: append($px-vals, $val-in-px); + } + $rem-vals: scut-rem($pixels); + + #{$property}: $px-vals; + #{$property}: $rem-vals; + +} +@mixin scut-reset-border-box { + // Make everything a border-box, because why not? + html { + box-sizing: border-box; + } + *, *:before, *:after { + box-sizing: inherit; + } +} + +@mixin scut-reset-antialias { + // Antialias! + body { + -webkit-font-smoothing: antialiased; + } + *, *:before, *:after { + -webkit-font-smoothing: inherit; + } +} + +@mixin scut-reset-semanticize { + // Make headers and <b> semantic, not presentational. + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: 1em; + font-weight: normal; + margin: 0; + } + b { + font-weight: normal; + } +} + +@mixin scut-reset-pointer { + // Clickable form elements should have a pointer. + label, + select, + option, + button { + cursor: pointer; + } +} + +@mixin scut-reset-form { + fieldset { + border: 0; + margin: 0; + padding: 0; + } + textarea { + resize: vertical; + } +} + +@mixin scut-reset-button { + // Reset default button styles, which are never used. + button, + input[type="button"], + input[type="submit"], + input[type="reset"] { + background: transparent; + border: 0; + color: inherit; + font: inherit; + margin: 0; + padding: 0; + width: auto; + -webkit-appearance: none; + -webkit-font-smoothing: antialiased; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + &::-moz-focus-inner { + padding: 0; + border: 0; + } + } +} + +@mixin scut-reset-paragraph { + // Some paragraph margins just get in the way. + p:first-of-type { + margin-top: 0; + } + p:last-of-type { + margin-bottom: 0; + } +} + +@mixin scut-reset-media { + // You want these elements fluid, probably. + img, + video { + max-width: 100%; + height: auto; + } +} + +@mixin scut-reset-figure { + // Remove default margins. + figure { + margin: 0; + } +} + +// Call them all, minus exclusions! +@mixin scut-reset ($exclude: false) { + @if not(index($exclude, border-box)) { + @include scut-reset-border-box; + } + @if not(index($exclude, antialias)) { + @include scut-reset-antialias; + } + @if not(index($exclude, semanticize)) { + @include scut-reset-semanticize; + } + @if not(index($exclude, pointer)) { + @include scut-reset-pointer; + } + @if not(index($exclude, form)) { + @include scut-reset-form; + } + @if not(index($exclude, button)) { + @include scut-reset-button; + } + @if not(index($exclude, paragraph)) { + @include scut-reset-paragraph; + } + @if not(index($exclude, media)) { + @include scut-reset-media; + } + @if not(index($exclude, figure)) { + @include scut-reset-figure; + } +} + +@mixin scut-selected ( + $active: false +) { + + @if $active { + &:hover, + &:focus, + &:active { + @content; + } + } + @else { + &:hover, + &:focus { + @content; + } + } + +} +@mixin scut-triangle ( + $direction: right, + $size: 0.75em, + $color: inherit +) { + + display: inline-block; + height: 0; + width: 0; + // For improved appearance in some Webkit browsers + -webkit-transform: rotate(360deg); + + // Set up some variables + $width: null; + $height: null; + $border-widths: null; + + @if type-of($size) == list { + $width: nth($size, 1); + $height: nth($size, 2); + } + @else { + $width: $size; + $height: $size; + } + + @if ($direction == up) or ($direction == down) { + // For up and down, width gets two borders but height only one, + // so divide second border-width value by 2 + $border-widths: $height ($width / 2); + } + @else if ($direction == right) or ($direction == left) { + // For right and left, height gets two borders but width only one, + // so divide first border-width value by 2 + $border-widths: ($height / 2) $width; + } + @else { + // For right triangles (the rest), both sides get two borders, + // so divide both by 2 + $border-widths: ($height / 2) ($width / 2); + } + + border-width: $border-widths; + border-style: solid; + + + // STANDARD TRIANGLES + + @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) { + border-color: transparent; + @if $direction == up { + border-bottom-color: $color; + border-top-width: 0; + } + @else if $direction == right { + border-left-color: $color; + border-right-width: 0; + } + @else if $direction == down { + border-top-color: $color; + border-bottom-width: 0; + } + @else if $direction == left { + border-right-color: $color; + border-left-width: 0; + } + } + + + // CORNER TRIANGLES + + @else if ($direction == top-right) or ($direction == top-left) { + border-top-color: $color; + border-bottom-color: transparent; + @if $direction == top-right { + border-left-color: transparent; + border-right-color: $color; + } + @else if $direction == top-left { + border-left-color: $color; + border-right-color: transparent; + } + } + + @else if ($direction == bottom-right) or ($direction == bottom-left) { + border-top-color: transparent; + border-bottom-color: $color; + @if $direction == bottom-right { + border-left-color: transparent; + border-right-color: $color; + } + @else if $direction == bottom-left { + border-left-color: $color; + border-right-color: transparent; + } + } + +} + +%scut-triangle { + @include scut-triangle; +} +@mixin scut-center-absolutely ( + $dimensions +) { + + $width: nth($dimensions, 1); + $height: nth($dimensions, 2); + + position: absolute; + + @if $width != n { + width: $width; + left: 50%; + margin-left: (-$width / 2); + } + + @if $height != n { + height: $height; + top: 50%; + margin-top: (-$height / 2); + } + +} +@mixin scut-center-block ( + $max-width: false +) { + + margin-left: auto; + margin-right: auto; + @if $max-width { + max-width: $max-width; + } + +} + +%scut-center-block { + @include scut-center-block; +} + +@mixin scut-center-transform ( + $axis: false // or x or y +) { + + position: absolute; + + @if $axis != x { + top: 50%; + margin-top: auto; + margin-bottom: auto; + } + + @if $axis != y { + left: 50%; + margin-left: auto; + margin-right: auto; + } + + $translate-val: null; + + @if not($axis) { + $translate-val: translate(-50%, -50%); + } + @else if $axis != x { + $translate-val: translateY(-50%); + } + @else if $axis != y { + $translate-val: translateX(-50%); + } + + -webkit-transform: $translate-val; + -ms-transform: $translate-val; + transform: $translate-val; +} + +%scut-center-transform { + @include scut-center-transform; +} + +%scut-center-transform-x { + @include scut-center-transform(x); +} + +%scut-center-transform-y { + @include scut-center-transform(y); +} + +@mixin scut-fill ( + $width-height: false +) { + + position: absolute; + left: 0; + top: 0; + @if $width-height { + width: 100%; + height: 100%; + } + @else { + right: 0; + bottom: 0; + } + +} + +%scut-fill { + @include scut-fill; +} +@mixin scut-list-custom ( + $content: "\2022", + $marker-width: 0.75em, + $pad: 0, + $no-margin: false +) { + + $content-val: null; + $counter: index($content, count); + @if $counter { + @if length($content) == 3 { + $content-val: counter(scutlistcounter, nth($content, 3))nth($content,2); + } + @else if length($content) == 2 { + $content-val: counter(scutlistcounter)nth($content,2); + } + @else { + $content-val: counter(scutlistcounter); + } + } + @else { + $content-val: $content; + } + + padding-left: $marker-width + $pad; + list-style-type: none; + + @if $no-margin { + margin-top: 0; + margin-bottom: 0; + } + + & > li { + position: relative; + @if $counter { + counter-increment: scutlistcounter; + } + &:before { + content: $content-val; + display: block; + position: absolute; + top: 0; + left: -$marker-width; + width: $marker-width; + @content; + } + } + +} +// Depends on `list-floated`, which depends in turn on `list-unstyled` and `clearfix`. + +@mixin scut-list-divided ( + $divider: "|", + $space: 0.5em, + $dir: left, + $height: false, + $no-margin: true +) { + + @include scut-list-floated($dir: $dir, $no-margin: $no-margin); + + $pseudo: if($dir == left, 'before', 'after'); + + // If an explicit height is passed, + // things are different: All <li>s + // need the pseudo-element (to force height), + // but the first's must be hidden. + + @if $height { + & > li { + height: $height; + } + & > li:#{$pseudo} { + height: $height; + content: $divider; + display: inline-block; + vertical-align: middle; + @content; + } + & > li:first-child:#{$pseudo} { + width: 0; + overflow: hidden; + } + } + + & > li + li:#{$pseudo} { + @if not($height) { + content: $divider; + display: inline-block; + @content; + } + margin-left: $space; + margin-right: $space; + } + +} + +%scut-list-bar { + @include scut-list-divided; +} + +%scut-list-breadcrumb { + @include scut-list-divided("/"); +} +// Depends on `list-unstyled`. + +@mixin scut-list-inline ( + $space: false, + $no-margin: true +) { + + @include scut-list-unstyled($no-margin); + + & > li { + display: inline-block; + } + + @if $space { + & > li + li { + margin-left: $space; + } + } + +} + +%scut-list-inline { + @include scut-list-inline; +} +// Depends on `list-unstyled`. + +@mixin scut-list-punctuated ( + $divider: ", ", + $display: inline, + $no-margin: true +) { + + @include scut-list-unstyled($no-margin); + + & > li { + display: $display; + &:not(:last-child):after { + content: $divider; + } + } + +} + +%scut-list-comma { + @include scut-list-punctuated; +} +@mixin scut-margin ( + $margin +) { + + @if length($margin) == 1 and $margin != n { + margin-top: $margin; + margin-right: $margin; + margin-bottom: $margin; + margin-left: $margin; + } + + @if length($margin) == 2 { + $margin-y: nth($margin, 1); + $margin-x: nth($margin, 2); + @if $margin-y != n { + margin-top: $margin-y; + margin-bottom: $margin-y; + } + @if $margin-x != n { + margin-left: $margin-x; + margin-right: $margin-x; + } + } + + @if length($margin) == 3 { + $margin-y-top: nth($margin, 1); + $margin-x: nth($margin, 2); + $margin-y-bottom: nth($margin, 3); + @if $margin-y-top != n { + margin-top: $margin-y-top; + } + @if $margin-x != n { + margin-right: $margin-x; + margin-left: $margin-x; + } + @if $margin-y-bottom != n { + margin-bottom: $margin-y-bottom; + } + } + + @if length($margin) == 4 { + $margin-top: nth($margin, 1); + $margin-right: nth($margin, 2); + $margin-bottom: nth($margin, 3); + $margin-left: nth($margin, 4); + @if $margin-top != n { + margin-top: $margin-top; + } + @if $margin-right != n { + margin-right: $margin-right; + } + @if $margin-bottom != n { + margin-bottom: $margin-bottom; + } + @if $margin-left != n { + margin-left: $margin-left; + } + } + +} +@mixin scut-padding ( + $padding +) { + + @if length($padding) == 1 and $padding != n { + padding-top: $padding; + padding-right: $padding; + padding-bottom: $padding; + padding-left: $padding; + } + + @if length($padding) == 2 { + $padding-y: nth($padding, 1); + $padding-x: nth($padding, 2); + @if $padding-y != n { + padding-top: $padding-y; + padding-bottom: $padding-y; + } + @if $padding-x != n { + padding-left: $padding-x; + padding-right: $padding-x; + } + } + + @if length($padding) == 3 { + $padding-y-top: nth($padding, 1); + $padding-x: nth($padding, 2); + $padding-y-bottom: nth($padding, 3); + @if $padding-y-top != n { + padding-top: $padding-y-top; + } + @if $padding-x != n { + padding-right: $padding-x; + padding-left: $padding-x; + } + @if $padding-y-bottom != n { + padding-bottom: $padding-y-bottom; + } + } + + @if length($padding) == 4 { + $padding-top: nth($padding, 1); + $padding-right: nth($padding, 2); + $padding-bottom: nth($padding, 3); + $padding-left: nth($padding, 4); + @if $padding-top != n { + padding-top: $padding-top; + } + @if $padding-right != n { + padding-right: $padding-right; + } + @if $padding-bottom != n { + padding-bottom: $padding-bottom; + } + @if $padding-left != n { + padding-left: $padding-left; + } + } +} +// Depends on `positioning-coordinates`. + +@mixin scut-absolute ( + $coordinates: 0 n n 0 +) { + + position: absolute; + @include scut-coords($coordinates); + +} + +%scut-absolute { + @include scut-absolute; +} +// Depends on `positioning-coordinates`. + +@mixin scut-fixed ( + $coordinates: 0 n n 0 +) { + + position: fixed; + @include scut-coords($coordinates); + +} + +%scut-fixed { + @include scut-fixed; +} +// Depends on `positioning-coordinates`. + +@mixin scut-relative ( + $coordinates: n n n n +) { + + position: relative; + @include scut-coords($coordinates); + +} +@mixin scut-ratio-box ( + $ratio: 1/1 +) { + + overflow: hidden; + position: relative; + + // The container's height, as a percentage of the + // container's width, is set by assigning + // padding-top to a pseudo-element. + &:before { + content: ""; + display: block; + height: 0; + padding-top: (1 / $ratio) * 100%; + } + +} + +%scut-ratio-box { + @include scut-ratio-box; +} +@mixin scut-size( + $size +) { + + @if length($size) == 1 { + width: $size; + height: $size; + } + @else if length($size) == 2 { + width: nth($size, 1); + height: nth($size, 2); + } + +} +@mixin scut-sticky-footer-fixed ( + $height, + $wrapper: ".wrapper", + $footer: ".scut-sticky" +) { + + html, + body { + height: 100%; + margin: 0; + padding: 0; + } + + #{$wrapper} { + min-height: 100%; + margin-bottom: -$height; + &:after { + content: ""; + display: block; + } + } + + #{$wrapper}:after, + #{$footer} { + height: $height; + } + +} + +// deprecated +@mixin scut-sticky-footer ( + $height, + $wrapper: ".wrapper", + $footer: ".scut-sticky" +){ + @include scut-sticky-footer-fixed($height, $wrapper, $footer); +} +@mixin scut-sticky-footer-fluid ( + $wrapper: ".wrapper", + $footer: ".scut-sticky" +) { + + html, + body { + height: 100%; + margin: 0; + padding: 0; + } + + #{$wrapper} { + display: table; + height: 100%; + width: 100%; + } + + #{$footer} { + display: table-row; + height: 1px; + } + +} +@mixin scut-vcenter-ib ( + $inner... +) { + + // The inner element is vertically centered + // by middle-aligning it with an inline pseudo-element + // whose height is 100%. + + &:before { + content: ""; + height: 100%; + display: inline-block; + vertical-align: middle; + // A small negative right margin is set + // to account for the default + // word-spacing of inline-block. + margin-right: -0.25em; + } + + $inner: if(length($inner) == 0, ".scut-inner", $inner); + @each $cell-selector in $inner { + $cell-selector: unquote($cell-selector); + & > #{$cell-selector} { + display: inline-block; + vertical-align: middle; + } + } + +} + +%scut-vcenter-ib { + @include scut-vcenter-ib; +} + +@mixin scut-vcenter-lh ( + $height +) { + + height: $height; + line-height: $height; + +} +@mixin scut-vcenter-td ( + $inner... +) { + + display: table; + + $inner: if(length($inner) == 0, ".scut-inner", $inner); + @each $cell-selector in $inner { + $cell-selector: unquote($cell-selector); + & > #{$cell-selector} { + display: table-cell; + vertical-align: middle; + } + } + +} + + +%scut-vcenter-td { + @include scut-vcenter-td; +} + +// Depends on scut-center-transform + +@mixin scut-vcenter-tt () { + @include scut-center-transform(y); +} + +%scut-vcenter-tt { + @include scut-vcenter-tt; +} +// space +$scut-space: "\0020"; +// non-breaking space +$scut-nbsp: "\00a0"; + +// quotation mark +$scut-quot: "\0022"; +// left single curly quote +$scut-lsquo: "\2018"; +// right single curly quote +$scut-rsquo: "\2019"; +// left double curly quote +$scut-ldquo: "\201C"; +// right double curly quote +$scut-rdquo: "\201D"; +// left single angle quote (guillemet) +$scut-lsaquo: "\2039"; +// right single angle quote (guillemet) +$scut-rsaquo: "\203A"; +// left double angle quote (guillemet) +$scut-laquo: "\00ab"; +// right double angle quote (guillemet) +$scut-raquo: "\00bb"; + +// em dash (mutton) +$scut-mdash: "\2014"; +// en dash (nut) +$scut-ndash: "\2013"; +// hyphen +$scut-hyphen: "\2010"; + +// ampersand +$scut-amp: "\0026"; +// greater than +$scut-gt: "\003e"; +// less than +$scut-lt: "\003c"; +// times +$scut-times: "\00D7"; +// big times +$scut-bigtimes: "\2715"; +// checkmark +$scut-checkmark: "\2713"; + +// section sign (double S, hurricane, sectional symbol, the legal doughnut, signum sectionis) +$scut-sect: "\00a7"; +// paragraph symbol (pilcrow) +$scut-para: "\00b6"; + +// middot (interpunct, interpoint) +$scut-middot: "\00b7"; +// o-slash (slashed o) +$scut-oslash: "\00f8"; +// bullet +$scut-bull: "\2022"; +// white bullet +$scut-whibull: "\25E6"; +// horizontal ellipsis +$scut-hellip: "\2026"; +// vertical ellipsis +$scut-vellip: "\22EE"; +// midline horizontal ellipsis +$scut-midhellip: "\22EF"; + +// up-pointing triangle +$scut-utri: "\25b2"; +// down-pointing triangle +$scut-dtri: "\25bc"; +// left-pointing triangle +$scut-ltri: "\25c0"; +// right-pointing triangle +$scut-rtri: "\25b6"; +// up-pointing small triangle +$scut-ustri: "\25b4"; +// down-pointing small triangle +$scut-dstri: "\25be"; +// left-pointing small triangle +$scut-lstri: "\25c2"; +// right-pointing small triangle +$scut-rstri: "\25b8"; +// diamond +$scut-diamond: "\25c6"; +// fisheye +$scut-fisheye: "\25c9"; +// bullseye +$scut-bullseye: "\25ce"; +// circle +$scut-circle: "\25cf"; +// white circle +$scut-whitecircle: "\25cb"; +// square +$scut-square: "\25a0"; +// white square +$scut-whitesquare: "\25a1"; +// small square +$scut-ssquare: "\25aa"; +// small white square +$scut-swhitesquare: "\25ab"; +@function main-src($formats, $file-path, $font-family) { + // Return the list of `src` values, in order, that + // a good `@font-face` will need, including only + // those formats specified in the list `$formats`. + $result: (); + @if index($formats, eot) { + $eot-val: url('#{$file-path}.eot?#iefix') format('embedded-opentype'); + $result: append($result, $eot-val, comma); + } + @if index($formats, woff2) { + $woff2-val: url('#{$file-path}.woff2') format('woff2'); + $result: append($result, $woff2-val, comma); + } + @if index($formats, woff) { + $woff-val: url('#{$file-path}.woff') format('woff'); + $result: append($result, $woff-val, comma); + } + @if index($formats, ttf) { + $ttf-val: url('#{$file-path}.ttf') format('truetype'); + $result: append($result, $ttf-val, comma); + } + @if index($formats, svg) { + $svg-val: url('#{$file-path}.svg##{$font-family}') format('svg'); + $result: append($result, $svg-val, comma); + } + @return $result; +} + +@mixin scut-font-face ( + $font-family, + $file-path, + $weight: normal, + $style: normal, + $formats: eot woff2 woff ttf svg +) { + + @if index('italic' 'oblique', $weight) { + $style: $weight; + $weight: normal; + } + + @font-face { + font-family: $font-family; + font-weight: $weight; + font-style: $style; + + @if index($formats, eot) { + src: url('#{$file-path}.eot'); + } + src: main-src($formats, $file-path, $font-family); + } + +} + +@mixin scut-hanging-indent ( + $indent: 1em +) { + + // padding-left creates the indent, + // while text-indent pulls the first line + // back to the edge. + + padding-left: $indent; + text-indent: -$indent; + +} + +%scut-hanging-indent { + @include scut-hanging-indent; +} +@mixin scut-indented-ps ( + $indent: 1.5em, + $no-first-indent: true +) { + + p { + margin: 0; + text-indent: $indent; + } + + @if $no-first-indent { + p:first-of-type { + text-indent: 0; + } + } + +} + +%scut-indented-ps { + @include scut-indented-ps; +} +@mixin scut-key-val ( + $divider: ":", + $pad: 0.25em, + $indent: 1em, + $spacing: 0, + $pad-left: 0 +) { + + & > dt { + clear: both; + float: left; + &:after { + content: $divider; + margin-right: $pad; + @if $pad-left != 0 { + margin-left: $pad-left; + } + } + } + + & > dd { + margin-left: $indent; + @if $spacing != 0 { + margin-bottom: $spacing; + } + } + +} + +%scut-key-val { + @include scut-key-val; +} +@mixin scut-link-bb ( + $color: inherit, + $style: solid, + $width: 1px +) { + + text-decoration: none; + + border-bottom-width: $width; + border-bottom-style: $style; + @if $color != inherit { + border-bottom-color: $color; + } + +} + +%scut-link-bb { + @include scut-link-bb; +} +// SCUT LINK UNSTYLED +// http://davidtheclark.github.io/scut/#link-unstyled + +@mixin scut-link-unstyled() { + + text-decoration: none; + color: inherit; + +} + +%scut-link-unstyled { + @include scut-link-unstyled(); +} + +@mixin scut-reverse-italics ( + $elements: null +) { + + $element-list: em, cite, i; + font-style: italic; + #{join($element-list, $elements)} { + font-style: normal; + } + +} + +%scut-reverse-italics { + @include scut-reverse-italics; +} + +@mixin scut-side-lined ( + $height: 1px, + $space: 0.5em, + $color: inherit, + $style: solid, + $v-adjust: false, + $double: false +) { + + display: block; + overflow: hidden; + text-align: center; + + &:before, + &:after { + content: ""; + display: inline-block; + vertical-align: middle; + position: relative; + width: 50%; + + border-top-style: $style; + border-top-width: $height; + + @if $color != inherit { + border-top-color: $color; + } + + @if $v-adjust != false { + bottom: $v-adjust; + } + + @if $double != false { + height: $double; + border-bottom-style: $style; + border-bottom-width: $height; + @if $color != inherit { + border-bottom-color: $color; + } + } + } + + &:before { + right: $space; + margin-left: -50%; + } + &:after { + left: $space; + margin-right: -50%; + } + +} + +%scut-side-lined { + @include scut-side-lined; +} +@mixin scut-truncate { + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + +} + +%scut-truncate { + @include scut-truncate; +}
\ No newline at end of file diff --git a/web-ui/app/scss/views/_close-button.scss b/web-ui/app/scss/views/_close-button.scss new file mode 100644 index 00000000..37171c18 --- /dev/null +++ b/web-ui/app/scss/views/_close-button.scss @@ -0,0 +1,22 @@ +.close-mail-button { + $button-size: 27px; + + margin-right: 3px; + float: left; + background: $lighter_gray; + color: $medium_light_grey; + width: $button-size; + height: $button-size; + padding: 0; + border-radius: 0; + + &:hover, &:focus, &:active { + background-color: darken($lighter_gray, 2); + color: darken($medium_light_grey, 10); + } + + i { + padding: 0; + margin: 0; + } +} diff --git a/web-ui/app/scss/views/_compose-button.scss b/web-ui/app/scss/views/_compose-button.scss new file mode 100644 index 00000000..41814a38 --- /dev/null +++ b/web-ui/app/scss/views/_compose-button.scss @@ -0,0 +1,24 @@ +// 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; + @include btn-transition; + &:hover { + background: lighten($action_buttons, 10%); + cursor: pointer; + } + } + } +} + diff --git a/web-ui/app/scss/views/_compose-view.scss b/web-ui/app/scss/views/_compose-view.scss new file mode 100644 index 00000000..2ed305c8 --- /dev/null +++ b/web-ui/app/scss/views/_compose-view.scss @@ -0,0 +1,320 @@ +.compose-view { + overflow: auto; +} + +// 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; + cursor: text; + 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; + } + + #attachment-upload-item { + display: none; + + .progress { + width: 0%; + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + min-height: 100%; + + .progress-bar { + height: 100%; + background-color: rgba($light_blue, 0.3); + } + } + } + + .attachmentsAreaWrap { + padding: 0; + + .attachmentsArea { + padding: 0; + border-top: 0; + + #upload-error { + color: $error; + margin-bottom: 20px; + + .close-icon { + font-size: 1.0rem; + cursor: pointer; + } + + span, a { + color: $error; + font-size: 0.9rem; + } + + a { + text-decoration: underline; + padding: 5px; + } + } + } + } + + .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; + } + + .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: 4px; + } + + &.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: "\f13e "; + } + } + + &.deleting span { + text-decoration: line-through; + } + + & span { + margin: 0px; + padding: 0px; + 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: "Click to remove"; + 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; + } +} diff --git a/web-ui/app/scss/views/_message-panel.scss b/web-ui/app/scss/views/_message-panel.scss new file mode 100644 index 00000000..4a0a7a6b --- /dev/null +++ b/web-ui/app/scss/views/_message-panel.scss @@ -0,0 +1,26 @@ +.message-panel { + width: 100%; + margin: 10px auto; + position: fixed; + z-index: 10000; + text-align: center; + + &__growl { + padding: 5px 60px; + + &--success { + background: $warning; + color: darken($warning, 50%); + border: 1px solid darken($warning, 10%); + @include box-shadow(1px 1px 3px darken($warning, 60%)); + } + + &--error { + font-weight: bold; + color: white; + background: $error; + border: 1px solid darken($error, 10%); + @include box-shadow(1px 1px 3px darken($error, 60%)); + } + } +} diff --git a/web-ui/app/scss/views/_no-mails-available.scss b/web-ui/app/scss/views/_no-mails-available.scss new file mode 100644 index 00000000..bf5d256a --- /dev/null +++ b/web-ui/app/scss/views/_no-mails-available.scss @@ -0,0 +1,3 @@ +.no-mails-available-pane { + @extend .no-content-placeholder; +} diff --git a/web-ui/app/scss/views/_no-message-selected.scss b/web-ui/app/scss/views/_no-message-selected.scss new file mode 100644 index 00000000..0e367bf2 --- /dev/null +++ b/web-ui/app/scss/views/_no-message-selected.scss @@ -0,0 +1,14 @@ +.no-message-selected-pane { + background: $contrast; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + &__text { + @extend .no-content-placeholder; + + margin-bottom: 40px; // aligns label with "no results for XYZ" + } +} diff --git a/web-ui/app/scss/views/_read-view.scss b/web-ui/app/scss/views/_read-view.scss new file mode 100644 index 00000000..6c229f9b --- /dev/null +++ b/web-ui/app/scss/views/_read-view.scss @@ -0,0 +1,181 @@ +.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-x: 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 { + clear: both; + margin: 0 0 10px; + + // TODO refactor + + @include tags; + + ul li { + &.tag:hover { + &:before { + content: "click to remove"; + text-transform: lowercase; + font-size: 0.5rem; + + @include tooltip(18px, 8px); + } + } + } + } + } + + &__body { + margin: $component-vertical-spacing 0; + width: 100%; + border: none; + } + + &__attachments { + margin: $component-vertical-spacing 0; + + &-header { + font-weight: bold; + } + + &-item { + @include scut-clearfix; + + 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/templates/compose/compose_box.hbs b/web-ui/app/templates/compose/compose_box.hbs index 78447ec2..b88138b5 100644 --- a/web-ui/app/templates/compose/compose_box.hbs +++ b/web-ui/app/templates/compose/compose_box.hbs @@ -1,30 +1,32 @@ -{{> recipients }} - -<div class="clearfix"> - <a id="to-trigger" class="hide">{{t 'To'}}</a> - <a id="ccs-trigger" class="hide">{{t 'CC'}}</a> - <a id="bccs-trigger" class="hide">{{t 'BCC'}}</a> -</div> - <button class="close-mail-button"> <i class="fa fa-times"></i> </button> -<div class="floatlabel"> - <label class="floatlabel" for="subject">Subject</label> - <input class="floatlabel" name="subject" type="text" id="subject" value="{{subject}}" placeholder="{{t 'Subject'}}" - tabindex="4"/> -</div> -<div class="floatlabel"> - <label class="floatlabel" for="body">Body</label> - <textarea class="floatlabel" name="body" id="text-box" placeholder="{{t 'Body'}}" tabindex="5">{{body}}</textarea> -</div> -{{> attachments_list }} +<div class="compose-view"> + + {{> recipients }} + <div class="clearfix"> + <a id="to-trigger" class="hide">{{t 'To'}}</a> + <a id="ccs-trigger" class="hide">{{t 'CC'}}</a> + <a id="bccs-trigger" class="hide">{{t 'BCC'}}</a> + </div> + + <div class="floatlabel"> + <label class="floatlabel" for="subject">Subject</label> + <input class="floatlabel" name="subject" type="text" id="subject" value="{{subject}}" placeholder="{{t 'Subject'}}" tabindex="4"/> + </div> + <div class="floatlabel"> + <label class="floatlabel" for="body">Body</label> + <textarea class="floatlabel" name="body" id="text-box" placeholder="{{t 'Body'}}" tabindex="5">{{body}}</textarea> + </div> + + {{> attachments_list }} -<div class="buttons-group columns"> - <button id="send-button" tabindex="6"><i class="fa fa-send"></i></button> - <span id="attachment-button" tabindex="6"></span> - <button id="trash-button" tabindex="7">{{t 'trash-button'}}<i class="fa fa-trash-o"></i></button> - <div id="draft-save-status"></div> -</div> + <div class="buttons-group columns"> + <button id="send-button" tabindex="6"><i class="fa fa-send"></i></button> + <span id="attachment-button" tabindex="6"></span> + <button id="trash-button" tabindex="7">{{t 'trash-button'}}<i class="fa fa-trash-o"></i></button> + <div id="draft-save-status"></div> + </div> +</div> <!-- ./compose-view --> diff --git a/web-ui/app/templates/compose/no_mails_available.hbs b/web-ui/app/templates/compose/no_mails_available.hbs index 6388d7db..147f3533 100644 --- a/web-ui/app/templates/compose/no_mails_available.hbs +++ b/web-ui/app/templates/compose/no_mails_available.hbs @@ -1,7 +1,7 @@ -<div class="scene"> +<div class="no-mails-available-pane"> {{#if forSearch }} - <div class="text">{{t 'NO RESULTS FOR'}}: '{{ forSearch }}'.</div> + {{t 'NO RESULTS FOR'}}: '{{ forSearch }}'. {{else}} - <div class="text">{{t 'NO EMAILS IN'}} '{{ tag }}'.</div> + {{t 'NO EMAILS IN'}} '{{ tag }}'. {{/if}} </div> diff --git a/web-ui/app/templates/compose/no_message_selected.hbs b/web-ui/app/templates/compose/no_message_selected.hbs index 0442192d..71aa6267 100644 --- a/web-ui/app/templates/compose/no_message_selected.hbs +++ b/web-ui/app/templates/compose/no_message_selected.hbs @@ -1,3 +1,3 @@ -<div class="scene"> - <div class="text">{{t 'NOTHING SELECTED'}}.</div> +<div class="no-message-selected-pane"> + <div class="no-message-selected-pane__text">{{t 'NOTHING SELECTED'}}.</div> </div> diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs index f9ec084a..0bc9d999 100644 --- a/web-ui/app/templates/mails/full_view.hbs +++ b/web-ui/app/templates/mails/full_view.hbs @@ -1,29 +1,25 @@ +<button class="close-mail-button"> + <i class="fa fa-times"></i> +</button> -<div id="fullView-{{ ident }}" class="{{statuses}}"> - - <header class="msg-header row"> - - <button class="close-mail-button"> - <i class="fa fa-times"></i> - </button> - - - <div class="full-view-header"> +<div id="fullView-{{ ident }}" class="mail-read-view {{statuses}}"> + <header class="mail-read-view__header row"> + <!-- TODO --> <div class="column large-12 no-padding security-status"> {{#if signatureStatus}} <span class="{{signatureStatus}}"> {{t signatureStatus }} </span> - {{/if}} - {{#if encryptionStatus}} + {{/if}} {{#if encryptionStatus}} <span class="{{encryptionStatus}}"> {{t encryptionStatus }} </span> {{/if}} </div> - <div class="recipients column large-10 no-padding"> - <span class="from"> + + <div class="mail-read-view__header-recipients"> + <span class="mail-read-view__header-recipients--highlight-sender"> {{#if header.from }} {{ header.from }} {{else}} @@ -33,51 +29,55 @@ <i class="fa fa-long-arrow-right"></i> {{{formatRecipients header}}} </div> - <div class="recipients column large-2 text-right"> - <span class="received-date">{{ formatDate header.date }}</span> + + <div class="mail-read-view__header-date"> + {{ formatDate header.date }} </div> - <div class="headline-area"> - <h3 class="subjectArea column large-10 no-padding"> - <span class="subject">{{ header.subject }}</span> - <div class="tagsArea"> - <ul class="tags"> - <i class="tags-label fa fa-tags"></i> - {{#each tags }} - <li class="tag" data-tag="{{this}}">{{ this }}</li> - {{/each }} + <hr> - <li class="new-tag"> - <input type="text" id="new-tag-input" placeholder="{{t 'Press Enter to add tag'}}"/> - </li> - <li class="add-new"> - <button id="new-tag-button" class="no-style"><i class="fa fa-plus"></i></button> - </li> - </ul> - </div> - </h3> - <nav id="mail-actions" class="column large-2 no-padding"> - </nav> + <div class="mail-read-view__header-subject"> + <h3>{{ header.subject }}</h3> </div> - </div> + <nav id="mail-actions" class="mail-read-view__header-actions"></nav> + + <div class="mail-read-view__header-tags"> + <ul class="tags"> + <i class="tags-label fa fa-tags"></i> + {{#each tags }} + <li class="tag" data-tag="{{this}}">{{ this }}</li> + {{/each }} + + <li class="new-tag"> + <input type="text" id="new-tag-input" placeholder="{{t 'Press Enter to add tag'}}" + /> + </li> + + <li class="add-new"> + <button id="new-tag-button" class="no-style"><i class="fa fa-plus"></i> + </button> + </li> + </ul> + </div> </header> - <div class="bodyArea column large-12"> - </div> + <iframe class="mail-read-view__body" id="read-sandbox" sandbox="allow-popups allow-scripts" src="sandbox/sandbox.html" scrolling="no"></iframe> {{#if attachments}} - <div class="attachmentsAreaWrap"> - <div class="attachmentsArea column large-12"> - <p><strong><i class="fa fa-paperclip"></i> {{ attachments.length }} attachment(s):</strong></p> - <ul> - {{#each attachments }} - <li> - <a href="/attachment/{{ this.ident }}?content_type={{ this.content-type }}&encoding={{ this.encoding }}&filename={{ this.name }}">{{ this.name }} ({{ formatSize this.size}})<i class="fa fa-arrow-down download-icon"></i></a> - </li> - {{/each }} - </ul> + <hr> + + <div id="attachmentsArea" class="mail-read-view__attachments"> + <p class="mail-read-view__attachments-header"><i class="fa fa-paperclip"></i> {{ attachments.length }} attachment(s):</p> + <ul> + {{#each attachments }} + <li class="mail-read-view__attachments-item"> + <a class="mail-read-view__attachments-item-label" href="/attachment/{{ this.ident }}?content_type={{ this.content-type }}&encoding={{ this.encoding }}&filename={{ this.name }}">{{ this.name }} ({{ formatSize this.size}}) + <i class="fa fa-arrow-down mail-read-view__attachments-item-download"></i></a> + </li> + {{/each }} + </ul> </div> - </div> {{/if}} </div> + diff --git a/web-ui/app/templates/mails/mail_actions.hbs b/web-ui/app/templates/mails/mail_actions.hbs index 94b79ab9..0b717373 100644 --- a/web-ui/app/templates/mails/mail_actions.hbs +++ b/web-ui/app/templates/mails/mail_actions.hbs @@ -1,6 +1,6 @@ -<button id="reply-button-top" class="no-style"><i class="fa fa-reply"></i></button> -<button id="view-more-actions" class="no-style"><i class="fa fa-caret-down"></i></button> -<ul id="more-actions"> - <li><span id="reply-all-button-top">{{t 'Reply to All'}}</span></li> - <li><span id="delete-button-top">{{t 'Delete this message'}}</span></li> +<button id="reply-button-top" class="mail-read-view__header-actions-button mail-read-view__header-actions-button--reply"><i class="fa fa-reply"></i></button> +<button id="view-more-actions" class="mail-read-view__header-actions-button mail-read-view__header-actions-button--more"><i class="fa fa-caret-down"></i></button> +<ul id="more-actions" class="mail-read-view__header-actions-dropdown"> + <li id="reply-all-button-top" class="mail-read-view__header-actions-dropdown-entry">{{t 'Reply to All'}}</li> + <li id="delete-button-top" class="mail-read-view__header-actions-dropdown-entry">{{t 'Delete this message'}}</li> </ul> diff --git a/web-ui/app/templates/search/search_trigger.hbs b/web-ui/app/templates/search/search_trigger.hbs index 2261d154..f2c410a4 100644 --- a/web-ui/app/templates/search/search_trigger.hbs +++ b/web-ui/app/templates/search/search_trigger.hbs @@ -1,3 +1,3 @@ <form> - <input type="search" placeholder="{{t 'search-placeholder'}}"></input> + <input type="search" pattern="[a-zA-Z0-9\s]{3,}" placeholder="{{t 'search-placeholder'}}"></input> </form> diff --git a/web-ui/app/templates/user_alerts/message.hbs b/web-ui/app/templates/user_alerts/message.hbs index 3a0055eb..abba1f91 100644 --- a/web-ui/app/templates/user_alerts/message.hbs +++ b/web-ui/app/templates/user_alerts/message.hbs @@ -1 +1 @@ -<span class="{{ message.class }}">{{ message.content }}</span> +<span class="message-panel__growl {{ message.class }}">{{ message.content }}</span> diff --git a/web-ui/bower.json b/web-ui/bower.json index 263ac2e4..018a57c4 100644 --- a/web-ui/bower.json +++ b/web-ui/bower.json @@ -17,7 +17,8 @@ "jquery-file-upload": "~9.11.2", "jquery-ui": "~1.11.4", "DOMPurify": "~0.7.4", - "he": "~0.5.0" + "he": "~0.5.0", + "iframe-resizer": "~3.5.3" }, "devDependencies": { "handlebars": "2.0.0", diff --git a/web-ui/config/package.sh b/web-ui/config/package.sh index 9b6bc66c..0bd82367 100644 --- a/web-ui/config/package.sh +++ b/web-ui/config/package.sh @@ -28,16 +28,24 @@ mkdir -p dist ./go handlebars ./go imagemin ./go minify_html +./go minify_sandbox ./go add_git_version ./go buildmain # copy files cd app -cp --parents 404.html fonts/* locales/**/* bower_components/font-awesome/css/font-awesome.min.css bower_components/jquery-file-upload/css/jquery.fileupload.css bower_components/font-awesome/fonts/* ../dist +cp --parents \ +404.html \ +fonts/* \ +locales/**/* \ +bower_components/font-awesome/css/font-awesome.min.css \ +bower_components/jquery-file-upload/css/jquery.fileupload.css \ +bower_components/font-awesome/fonts/* \ +../dist cd - -# concat js files and minify +# concat js files and minify for app.min.js cat \ app/bower_components/modernizr/modernizr.js \ app/bower_components/lodash/dist/lodash.js \ @@ -51,6 +59,14 @@ 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.min.js \ .tmp/app.concatenated.js > dist/app.js node_modules/.bin/minify dist/app.js > dist/app.min.js rm dist/app.js + +# concat js files and minify for sandbox.min.js +cat \ +app/js/sandbox.js \ +app/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js > dist/sandbox.js +node_modules/.bin/minify dist/sandbox.js > dist/sandbox.min.js +rm dist/sandbox.js diff --git a/web-ui/package.json b/web-ui/package.json index a49e32d1..0e8b9262 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -38,6 +38,7 @@ "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|<!--usemin_start-->.*<!--usemin_end-->|<script src=\"assets/app.min.js\" type=\"text/javascript\"></script>|' > dist/index.html", + "minify_sandbox": "node_modules/.bin/html-minifier app/sandbox.html --collapse-whitespace | sed 's|<!--usemin_start-->.*<!--usemin_end-->|<script src=\"sandbox.min.js\" type=\"text/javascript\"></script>|' > dist/sandbox.html", "add_git_version": "/bin/bash config/add_git_version.sh" } } diff --git a/web-ui/test/spec/helpers/sanitizer.spec.js b/web-ui/test/spec/helpers/sanitizer.spec.js index acd4b2b2..b553583e 100644 --- a/web-ui/test/spec/helpers/sanitizer.spec.js +++ b/web-ui/test/spec/helpers/sanitizer.spec.js @@ -25,6 +25,12 @@ define(['helpers/sanitizer'], function (sanitizer) { var output = sanitizer.purifyText('123<a>asd</a>'); expect(output).toEqual(expectedOutput); }); + + it('should leave highlighted text untouched', function () { + var expectedOutput = '<em class="search-highlight">123<a>asd</a></em>'; + var output = sanitizer.purifyText('<em class="search-highlight">123<a>asd</a></em>'); + expect(output).toEqual(expectedOutput); + }); }); describe('sanitizer.sanitize', function () { 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..29be6c2d 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 @@ -21,6 +21,12 @@ describeComponent('mail_view/ui/mail_view', function () { expect(spyEvent.mostRecentCall.data.mail).toEqual(1); }); + it('triggers mail.highlightMailContent when receiving mail.here', function () { + var hightlightEvent = spyOnEvent(document,Pixelated.events.mail.highlightMailContent); + this.component.trigger(this.component, Pixelated.events.mail.here); + expect(hightlightEvent).toHaveBeenTriggeredOn(document); + }); + it('triggers dispatchers.rightPane.openNoMessageSelected when getting mail.notFound', function () { var openNoMessageSelectedEvent = spyOnEvent(document, Pixelated.events.dispatchers.rightPane.openNoMessageSelected); @@ -29,6 +35,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); @@ -270,7 +281,7 @@ describeComponent('mail_view/ui/mail_view', function () { this.component.displayMail({}, withAttachments); - var attachmentLink = $(this.component.$node.find('.attachmentsArea li').html()); + var attachmentLink = $(this.component.$node.find('.mail-read-view__attachments-item').html()); var expectedLink = '/attachment/912ec803b2ce49e4a541068d495ab570?content_type=text/plain&encoding=base64&filename=filename.txt'; expect(attachmentLink.attr('href')) .toBe(expectedLink); }); diff --git a/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js b/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js index 157e6c47..1bf79b96 100644 --- a/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js +++ b/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js @@ -4,17 +4,17 @@ describeComponent('mail_view/ui/no_mails_available_pane', function () { describe('after initialization', function () { it('renders template', function () { this.setupComponent({tag: 'inbox'}); - expect(this.$node.html()).toMatch('<div class="text">NO EMAILS IN \'INBOX\'.</div>'); + expect(this.$node.html()).toMatch('<div class="no-mails-available-pane">\n NO EMAILS IN \'INBOX\'.\n</div>'); }); it('show different message for search with no results', function () { this.setupComponent({tag: 'all', forSearch: 'search'}); - expect(this.$node.html()).toMatch('<div class="text">NO RESULTS FOR: \'SEARCH\'.</div>'); + expect(this.$node.html()).toMatch('<div class="no-mails-available-pane">\n NO RESULTS FOR: \'SEARCH\'.\n</div>'); }); it('show only tag information when listing all mails', function () { this.setupComponent({tag: 'all', forSearch: 'in:all'}); - expect(this.$node.html()).toMatch('<div class="text">NO EMAILS IN \'ALL\'.</div>'); + expect(this.$node.html()).toMatch('<div class="no-mails-available-pane">\n NO EMAILS IN \'ALL\'.\n</div>'); }); }); }); 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(); diff --git a/web-ui/test/spec/search/results_highlighter.spec.js b/web-ui/test/spec/search/results_highlighter.spec.js index cfb61e9c..13131a8e 100644 --- a/web-ui/test/spec/search/results_highlighter.spec.js +++ b/web-ui/test/spec/search/results_highlighter.spec.js @@ -1,9 +1,11 @@ describeComponent('search/results_highlighter', function () { 'use strict'; - it('highlights words or parts of words that match with the keywords given', function () { + beforeEach(function () { this.setupComponent('<div id="text">Any one seeing too many open bugs</div>'); + }); + it('highlights words or parts of words that match with the keywords given', function () { this.component.attr = {keywords: ['any']}; this.component.highlightResults(event, {where: '#text'}); @@ -12,9 +14,15 @@ describeComponent('search/results_highlighter', function () { expect(highlightedWords).toEqual(2); }); - it('resets highlights when a new search is performed', function() { - this.setupComponent('<div id="text">Any one seeing too many open bugs</div>'); + it('highlights a string with the keywords given', function () { + this.component.attr = {keywords: ['foo']}; + var expectedString = 'the <em class="search-highlight">foo</em> bar'; + var string = this.component.highlightString('the foo bar'); + + expect(string).toEqual(expectedString); + }); + it('resets highlights when a new search is performed', function() { this.component.attr = {keywords: ['any']}; this.component.highlightResults(event, {where: '#text'}); $(document).trigger(Pixelated.events.search.resetHighlight); diff --git a/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js b/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js index bde3b7fa..f15cda81 100644 --- a/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js +++ b/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js @@ -8,13 +8,13 @@ describeComponent('user_alerts/ui/user_alerts', function () { it('should render message when ui:user_alerts:displayMessage is triggered', function () { this.component.trigger(Pixelated.events.ui.userAlerts.displayMessage, { message: 'a message' }); - expect(this.component.$node.html()).toEqual('<span class="success">a message</span>\n'); + expect(this.component.$node.html()).toEqual('<span class="message-panel__growl message-panel__growl--success">a message</span>\n'); }); it('should render error message', function () { this.component.trigger(Pixelated.events.ui.userAlerts.displayMessage, { message: 'send failed', class: 'error' }); - expect(this.component.$node.html()).toEqual('<span class="error">send failed</span>\n'); + expect(this.component.$node.html()).toEqual('<span class="message-panel__growl message-panel__growl--error">send failed</span>\n'); }); it('should be emptied and hidden when hide is called', function() { @@ -24,6 +24,4 @@ describeComponent('user_alerts/ui/user_alerts', function () { expect(this.$node.html()).toEqual(''); }); - - }); |