From e7ab9bf8b4e4897b99937d43b9e4810462dd2714 Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Mon, 16 Oct 2017 00:30:09 -0700
Subject: Refactor and simplify icon_button.js (#5413)

---
 app/javascript/mastodon/components/icon_button.js | 56 +++++++++++------------
 1 file changed, 28 insertions(+), 28 deletions(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index 3e5f8ac8c..68d1a2735 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -2,6 +2,7 @@ import React from 'react';
 import Motion from 'react-motion/lib/Motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
+import classNames from 'classnames';
 
 export default class IconButton extends React.PureComponent {
 
@@ -50,42 +51,41 @@ export default class IconButton extends React.PureComponent {
       ...(this.props.active ? this.props.activeStyle : {}),
     };
 
-    const classes = ['icon-button'];
+    const {
+      active,
+      animate,
+      className,
+      disabled,
+      expanded,
+      icon,
+      inverted,
+      overlay,
+      pressed,
+      tabIndex,
+      title,
+    } = this.props;
 
-    if (this.props.active) {
-      classes.push('active');
-    }
-
-    if (this.props.disabled) {
-      classes.push('disabled');
-    }
-
-    if (this.props.inverted) {
-      classes.push('inverted');
-    }
-
-    if (this.props.overlay) {
-      classes.push('overlayed');
-    }
-
-    if (this.props.className) {
-      classes.push(this.props.className);
-    }
+    const classes = classNames(className, 'icon-button', {
+      active,
+      disabled,
+      inverted,
+      overlayed: overlay,
+    });
 
     return (
-      <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
+      <Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
         {({ rotate }) =>
           <button
-            aria-label={this.props.title}
-            aria-pressed={this.props.pressed}
-            aria-expanded={this.props.expanded}
-            title={this.props.title}
-            className={classes.join(' ')}
+            aria-label={title}
+            aria-pressed={pressed}
+            aria-expanded={expanded}
+            title={title}
+            className={classes}
             onClick={this.handleClick}
             style={style}
-            tabIndex={this.props.tabIndex}
+            tabIndex={tabIndex}
           >
-            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
+            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
           </button>
         }
       </Motion>
-- 
cgit 


From 93b54b8d4b51f87c6e9cf642d5f57f557e9cd555 Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Mon, 16 Oct 2017 00:31:47 -0700
Subject: i18n "More" dropdown title (#5410)

---
 app/javascript/mastodon/components/status_action_bar.js | 3 ++-
 app/javascript/mastodon/locales/en.json                 | 1 +
 app/javascript/mastodon/locales/fr.json                 | 1 +
 3 files changed, 4 insertions(+), 1 deletion(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 803b730d9..e952733f3 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -13,6 +13,7 @@ const messages = defineMessages({
   block: { id: 'account.block', defaultMessage: 'Block @{name}' },
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
   share: { id: 'status.share', defaultMessage: 'Share' },
+  more: { id: 'status.more', defaultMessage: 'More' },
   replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
   reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
   cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
@@ -179,7 +180,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
         {shareButton}
 
         <div className='status__action-bar-dropdown'>
-          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
+          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 12efe0e0c..7e8d30c64 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -179,6 +179,7 @@
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mention @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 145b683f3..f192f3cfc 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -179,6 +179,7 @@
   "status.load_more": "Charger plus",
   "status.media_hidden": "Mรฉdia cachรฉ",
   "status.mention": "Mentionner",
+  "status.more": "Plus",
   "status.mute_conversation": "Masquer la conversation",
   "status.open": "Dรฉplier ce statut",
   "status.pin": "ร‰pingler sur le profil",
-- 
cgit 


From d5b767c3747b9e7f9afcbcecffb662843ca2a346 Mon Sep 17 00:00:00 2001
From: Yamagishi Kazutoshi <ykzts@desire.sh>
Date: Mon, 16 Oct 2017 16:33:08 +0900
Subject: Replace JavaScript Testing Framework from Mocha to Jest (#5412)

---
 .eslintrc.yml                                      |   1 +
 .travis.yml                                        |   2 +-
 .../__tests__/__snapshots__/avatar-test.js.snap    |  33 +
 .../__snapshots__/avatar_overlay-test.js.snap      |  24 +
 .../__tests__/__snapshots__/button-test.js.snap    | 114 +++
 .../__snapshots__/display_name-test.js.snap        |  23 +
 .../mastodon/components/__tests__/avatar-test.js   |  36 +
 .../components/__tests__/avatar_overlay-test.js    |  29 +
 .../mastodon/components/__tests__/button-test.js   |  75 ++
 .../components/__tests__/display_name-test.js      |  18 +
 .../features/emoji/__tests__/emoji-test.js         |  61 ++
 .../features/emoji/__tests__/emoji_index-test.js   | 130 +++
 .../ui/components/__tests__/column-test.js         |  34 +
 app/javascript/mastodon/test_setup.js              |   5 +
 jest.config.js                                     |  17 +
 package.json                                       |  28 +-
 spec/javascript/.eslintrc.yml                      |   3 -
 spec/javascript/components/avatar.test.js          |  44 -
 spec/javascript/components/avatar_overlay.test.js  |  36 -
 spec/javascript/components/button.test.js          |  72 --
 spec/javascript/components/display_name.test.js    |  18 -
 spec/javascript/components/emoji_index.test.js     | 111 ---
 spec/javascript/components/emojify.test.js         |  61 --
 .../features/ui/components/column.test.js          |  30 -
 spec/javascript/setup.js                           |  15 -
 yarn.lock                                          | 972 ++++++++++++++-------
 26 files changed, 1288 insertions(+), 704 deletions(-)
 create mode 100644 app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
 create mode 100644 app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
 create mode 100644 app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
 create mode 100644 app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
 create mode 100644 app/javascript/mastodon/components/__tests__/avatar-test.js
 create mode 100644 app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
 create mode 100644 app/javascript/mastodon/components/__tests__/button-test.js
 create mode 100644 app/javascript/mastodon/components/__tests__/display_name-test.js
 create mode 100644 app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
 create mode 100644 app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
 create mode 100644 app/javascript/mastodon/features/ui/components/__tests__/column-test.js
 create mode 100644 app/javascript/mastodon/test_setup.js
 create mode 100644 jest.config.js
 delete mode 100644 spec/javascript/.eslintrc.yml
 delete mode 100644 spec/javascript/components/avatar.test.js
 delete mode 100644 spec/javascript/components/avatar_overlay.test.js
 delete mode 100644 spec/javascript/components/button.test.js
 delete mode 100644 spec/javascript/components/display_name.test.js
 delete mode 100644 spec/javascript/components/emoji_index.test.js
 delete mode 100644 spec/javascript/components/emojify.test.js
 delete mode 100644 spec/javascript/components/features/ui/components/column.test.js
 delete mode 100644 spec/javascript/setup.js

(limited to 'app')

diff --git a/.eslintrc.yml b/.eslintrc.yml
index 1c60cbdb3..0172d7a9d 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -5,6 +5,7 @@ env:
   browser: true
   node: true
   es6: true
+  jest: true
 
 parser: babel-eslint
 
diff --git a/.travis.yml b/.travis.yml
index 52ff15c01..71b3a6069 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -53,5 +53,5 @@ before_script:
 
 script:
   - travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
-  - npm test
+  - yarn test
   - bundle exec i18n-tasks unused
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
new file mode 100644
index 000000000..76ab3374a
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
+<div
+  className="account__avatar"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  style={
+    Object {
+      "backgroundImage": "url(/animated/alice.gif)",
+      "backgroundSize": "100px 100px",
+      "height": "100px",
+      "width": "100px",
+    }
+  }
+/>
+`;
+
+exports[`<Avatar /> Still renders a still avatar 1`] = `
+<div
+  className="account__avatar"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  style={
+    Object {
+      "backgroundImage": "url(/static/alice.jpg)",
+      "backgroundSize": "100px 100px",
+      "height": "100px",
+      "width": "100px",
+    }
+  }
+/>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
new file mode 100644
index 000000000..d59fee42f
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<AvatarOverlay renders a overlay avatar 1`] = `
+<div
+  className="account__avatar-overlay"
+>
+  <div
+    className="account__avatar-overlay-base"
+    style={
+      Object {
+        "backgroundImage": "url(/static/alice.jpg)",
+      }
+    }
+  />
+  <div
+    className="account__avatar-overlay-overlay"
+    style={
+      Object {
+        "backgroundImage": "url(/static/eve.jpg)",
+      }
+    }
+  />
+</div>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
new file mode 100644
index 000000000..c3f018d90
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
@@ -0,0 +1,114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
+<button
+  className="button button-secondary"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders a button element 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
+<button
+  className="button"
+  disabled={true}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders class="button--block" if props.block given 1`] = `
+<button
+  className="button button--block"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders the children 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  <p>
+    children
+  </p>
+</button>
+`;
+
+exports[`<Button /> renders the given text 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  foo
+</button>
+`;
+
+exports[`<Button /> renders the props.text instead of children 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  foo
+</button>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
new file mode 100644
index 000000000..533359ffe
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<DisplayName /> renders display name + account name 1`] = `
+<span
+  className="display-name"
+>
+  <strong
+    className="display-name__html"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>Foo</p>",
+      }
+    }
+  />
+   
+  <span
+    className="display-name__account"
+  >
+    @
+    bar@baz
+  </span>
+</span>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/avatar-test.js b/app/javascript/mastodon/components/__tests__/avatar-test.js
new file mode 100644
index 000000000..dd3f7b7d2
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/avatar-test.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS } from 'immutable';
+import Avatar from '../avatar';
+
+describe('<Avatar />', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+
+  const size     = 100;
+
+  describe('Autoplay', () => {
+    it('renders a animated avatar', () => {
+      const component = renderer.create(<Avatar account={account} animate size={size} />);
+      const tree      = component.toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  describe('Still', () => {
+    it('renders a still avatar', () => {
+      const component = renderer.create(<Avatar account={account} size={size} />);
+      const tree      = component.toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  // TODO add autoplay test if possible
+});
diff --git a/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js b/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
new file mode 100644
index 000000000..44addea83
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS } from 'immutable';
+import AvatarOverlay from '../avatar_overlay';
+
+describe('<AvatarOverlay', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+
+  const friend = fromJS({
+    username: 'eve',
+    acct: 'eve@blackhat.lair',
+    display_name: 'Evelyn',
+    avatar: '/animated/eve.gif',
+    avatar_static: '/static/eve.jpg',
+  });
+
+  it('renders a overlay avatar', () => {
+    const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/__tests__/button-test.js b/app/javascript/mastodon/components/__tests__/button-test.js
new file mode 100644
index 000000000..160cd3cbc
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/button-test.js
@@ -0,0 +1,75 @@
+import { shallow } from 'enzyme';
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Button from '../button';
+
+describe('<Button />', () => {
+  it('renders a button element', () => {
+    const component = renderer.create(<Button />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the given text', () => {
+    const text      = 'foo';
+    const component = renderer.create(<Button text={text} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('handles click events using the given handler', () => {
+    const handler = jest.fn();
+    const button  = shallow(<Button onClick={handler} />);
+    button.find('button').simulate('click');
+
+    expect(handler.mock.calls.length).toEqual(1);
+  });
+
+  it('does not handle click events if props.disabled given', () => {
+    const handler = jest.fn();
+    const button  = shallow(<Button onClick={handler} disabled />);
+    button.find('button').simulate('click');
+
+    expect(handler.mock.calls.length).toEqual(0);
+  });
+
+  it('renders a disabled attribute if props.disabled given', () => {
+    const component = renderer.create(<Button disabled />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the children', () => {
+    const children  = <p>children</p>;
+    const component = renderer.create(<Button>{children}</Button>);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the props.text instead of children', () => {
+    const text      = 'foo';
+    const children  = <p>children</p>;
+    const component = renderer.create(<Button text={text}>{children}</Button>);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders class="button--block" if props.block given', () => {
+    const component = renderer.create(<Button block />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('adds class "button-secondary" if props.secondary given', () => {
+    const component = renderer.create(<Button secondary />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/__tests__/display_name-test.js b/app/javascript/mastodon/components/__tests__/display_name-test.js
new file mode 100644
index 000000000..0d040c4cd
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/display_name-test.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS }  from 'immutable';
+import DisplayName from '../display_name';
+
+describe('<DisplayName />', () => {
+  it('renders display name + account name', () => {
+    const account = fromJS({
+      username: 'bar',
+      acct: 'bar@baz',
+      display_name_html: '<p>Foo</p>',
+    });
+    const component = renderer.create(<DisplayName account={account} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
new file mode 100644
index 000000000..636402172
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -0,0 +1,61 @@
+import emojify from '../emoji';
+
+describe('emoji', () => {
+  describe('.emojify', () => {
+    it('ignores unknown shortcodes', () => {
+      expect(emojify(':foobarbazfake:')).toEqual(':foobarbazfake:');
+    });
+
+    it('ignores shortcodes inside of tags', () => {
+      expect(emojify('<p data-foo=":smile:"></p>')).toEqual('<p data-foo=":smile:"></p>');
+    });
+
+    it('works with unclosed tags', () => {
+      expect(emojify('hello>')).toEqual('hello>');
+      expect(emojify('<hello')).toEqual('<hello');
+    });
+
+    it('works with unclosed shortcodes', () => {
+      expect(emojify('smile:')).toEqual('smile:');
+      expect(emojify(':smile')).toEqual(':smile');
+    });
+
+    it('does unicode', () => {
+      expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
+        '<img draggable="false" class="emojione" alt="๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
+      expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).toEqual(
+      '<img draggable="false" class="emojione" alt="๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
+      expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
+      expect(emojify('\u2757')).toEqual(
+      '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" />');
+    });
+
+    it('does multiple unicode', () => {
+      expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
+        '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" />');
+      expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
+        '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" />');
+      expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
+        '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" />');
+      expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
+        'foo <img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
+    });
+
+    it('ignores unicode inside of tags', () => {
+      expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).toEqual('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
+    });
+
+    it('does multiple emoji properly (issue 5188)', () => {
+      expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ‘Œ" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="๐ŸŒˆ" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="๐Ÿ’•" title=":two_hearts:" src="/emoji/1f495.svg" />');
+      expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ‘Œ" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="๐ŸŒˆ" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="๐Ÿ’•" title=":two_hearts:" src="/emoji/1f495.svg" />');
+    });
+
+    it('does an emoji that has no shortcode', () => {
+      expect(emojify('๐Ÿ•‰๏ธ')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ•‰๏ธ" title="" src="/emoji/1f549.svg" />');
+    });
+
+    it('does an emoji whose filename is irregular', () => {
+      expect(emojify('โ†™๏ธ')).toEqual('<img draggable="false" class="emojione" alt="โ†™๏ธ" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
+    });
+  });
+});
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
new file mode 100644
index 000000000..53efa5743
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
@@ -0,0 +1,130 @@
+import { pick } from 'lodash';
+import { emojiIndex } from 'emoji-mart';
+import { search } from '../emoji_mart_search_light';
+
+const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
+
+describe('emoji_index', () => {
+  it('should give same result for emoji_index_light and emoji-mart', () => {
+    const expected = [
+      {
+        id: 'pineapple',
+        unified: '1f34d',
+        native: '๐Ÿ',
+      },
+    ];
+    expect(search('pineapple').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('orders search results correctly', () => {
+    const expected = [
+      {
+        id: 'apple',
+        unified: '1f34e',
+        native: '๐ŸŽ',
+      },
+      {
+        id: 'pineapple',
+        unified: '1f34d',
+        native: '๐Ÿ',
+      },
+      {
+        id: 'green_apple',
+        unified: '1f34f',
+        native: '๐Ÿ',
+      },
+      {
+        id: 'iphone',
+        unified: '1f4f1',
+        native: '๐Ÿ“ฑ',
+      },
+    ];
+    expect(search('apple').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('handles custom emoji', () => {
+    const custom = [
+      {
+        id: 'mastodon',
+        name: 'mastodon',
+        short_names: ['mastodon'],
+        text: '',
+        emoticons: [],
+        keywords: ['mastodon'],
+        imageUrl: 'http://example.com',
+        custom: true,
+      },
+    ];
+    search('', { custom });
+    emojiIndex.search('', { custom });
+    const expected = [
+      {
+        id: 'mastodon',
+        custom: true,
+      },
+    ];
+    expect(search('masto').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('should filter only emojis we care about, exclude pineapple', () => {
+    const emojisToShowFilter = unified => unified !== '1F34D';
+    expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
+      .not.toContain('pineapple');
+    expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
+      .not.toContain('pineapple');
+  });
+
+  it('can include/exclude categories', () => {
+    expect(search('flag', { include: ['people'] })).toEqual([]);
+    expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
+  });
+
+  it('does an emoji whose unified name is irregular', () => {
+    const expected = [
+      {
+        'id': 'water_polo',
+        'unified': '1f93d',
+        'native': '๐Ÿคฝ',
+      },
+      {
+        'id': 'man-playing-water-polo',
+        'unified': '1f93d-200d-2642-fe0f',
+        'native': '๐Ÿคฝโ€โ™‚๏ธ',
+      },
+      {
+        'id': 'woman-playing-water-polo',
+        'unified': '1f93d-200d-2640-fe0f',
+        'native': '๐Ÿคฝโ€โ™€๏ธ',
+      },
+    ];
+    expect(search('polo').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('can search for thinking_face', () => {
+    const expected = [
+      {
+        id: 'thinking_face',
+        unified: '1f914',
+        native: '๐Ÿค”',
+      },
+    ];
+    expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('can search for woman-facepalming', () => {
+    const expected = [
+      {
+        id: 'woman-facepalming',
+        unified: '1f926-200d-2640-fe0f',
+        native: '๐Ÿคฆโ€โ™€๏ธ',
+      },
+    ];
+    expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
+  });
+});
diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
new file mode 100644
index 000000000..1e5e1d8dc
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import Column from '../column';
+import ColumnHeader from '../column_header';
+
+describe('<Column />', () => {
+  describe('<ColumnHeader /> click handler', () => {
+    const originalRaf = global.requestAnimationFrame;
+
+    beforeEach(() => {
+      global.requestAnimationFrame = jest.fn();
+    });
+
+    afterAll(() => {
+      global.requestAnimationFrame = originalRaf;
+    });
+
+    it('runs the scroll animation if the column contains scrollable content', () => {
+      const wrapper = mount(
+        <Column heading='notifications'>
+          <div className='scrollable' />
+        </Column>
+      );
+      wrapper.find(ColumnHeader).simulate('click');
+      expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
+    });
+
+    it('does not try to scroll if there is no scrollable content', () => {
+      const wrapper = mount(<Column heading='notifications' />);
+      wrapper.find(ColumnHeader).simulate('click');
+      expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
+    });
+  });
+});
diff --git a/app/javascript/mastodon/test_setup.js b/app/javascript/mastodon/test_setup.js
new file mode 100644
index 000000000..80148379b
--- /dev/null
+++ b/app/javascript/mastodon/test_setup.js
@@ -0,0 +1,5 @@
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+const adapter = new Adapter();
+configure({ adapter });
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 000000000..dd9dadf87
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,17 @@
+module.exports = {
+  projects: [
+    '<rootDir>/app/javascript/mastodon',
+  ],
+  testPathIgnorePatterns: [
+    '<rootDir>/node_modules/',
+    '<rootDir>/vendor/',
+    '<rootDir>/config/',
+    '<rootDir>/log/',
+    '<rootDir>/public/',
+    '<rootDir>/tmp/',
+  ],
+  setupFiles: [
+    'raf/polyfill',
+  ],
+  setupTestFrameworkScriptFile: '<rootDir>/app/javascript/mastodon/test_setup.js',
+};
diff --git a/package.json b/package.json
index 93e254abc..cf8069e94 100644
--- a/package.json
+++ b/package.json
@@ -7,9 +7,9 @@
     "build:production": "cross-env RAILS_ENV=production ./bin/webpack",
     "manage:translations": "node ./config/webpack/translationRunner.js",
     "start": "node ./streaming/index.js",
-    "test": "npm run test:lint && npm run test:mocha",
+    "test": "npm run test:lint && npm run test:jest",
     "test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ streaming/",
-    "test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/**/*.test.js",
+    "test:jest": "cross-env NODE_ENV=test jest",
     "postinstall": "npm rebuild node-sass"
   },
   "repository": {
@@ -118,22 +118,36 @@
   },
   "devDependencies": {
     "babel-eslint": "^7.2.3",
-    "chai": "^4.1.0",
-    "chai-enzyme": "^0.8.0",
     "enzyme": "^3.0.0",
     "enzyme-adapter-react-16": "^1.0.0",
     "eslint": "^3.19.0",
     "eslint-plugin-jsx-a11y": "^4.0.0",
     "eslint-plugin-react": "^6.10.3",
-    "jsdom": "^11.1.0",
-    "mocha": "^3.4.1",
+    "jest": "^21.2.1",
+    "raf": "^3.4.0",
     "react-intl-translations-manager": "^5.0.0",
     "react-test-renderer": "^16.0.0",
-    "sinon": "^2.3.7",
     "webpack-dev-server": "^2.6.1",
     "yargs": "^8.0.2"
   },
   "optionalDependencies": {
     "fsevents": "*"
+  },
+  "jest": {
+    "projects": [
+      "<rootDir>/app/javascript/mastodon"
+    ],
+    "testPathIgnorePatterns": [
+      "<rootDir>/node_modules/",
+      "<rootDir>/vendor/",
+      "<rootDir>/config/",
+      "<rootDir>/log/",
+      "<rootDir>/public/",
+      "<rootDir>/tmp/"
+    ],
+    "setupFiles": [
+      "raf/polyfill"
+    ],
+    "setupTestFrameworkScriptFile": "<rootDir>/app/javascript/mastodon/test_setup.js"
   }
 }
diff --git a/spec/javascript/.eslintrc.yml b/spec/javascript/.eslintrc.yml
deleted file mode 100644
index 6db2a46c5..000000000
--- a/spec/javascript/.eslintrc.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-env:
-  mocha: true
diff --git a/spec/javascript/components/avatar.test.js b/spec/javascript/components/avatar.test.js
deleted file mode 100644
index 34949f2b5..000000000
--- a/spec/javascript/components/avatar.test.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import Avatar from '../../../app/javascript/mastodon/components/avatar';
-
-import { expect } from 'chai';
-import { render } from 'enzyme';
-import { fromJS }  from 'immutable';
-
-describe('<Avatar />', () => {
-  const account = fromJS({
-    username: 'alice',
-    acct: 'alice',
-    display_name: 'Alice',
-    avatar: '/animated/alice.gif',
-    avatar_static: '/static/alice.jpg',
-  });
-
-  const size = 100;
-  const animated = render(<Avatar account={account} animate size={size} />);
-  const still = render(<Avatar account={account} size={size} />);
-
-  // Autoplay
-  xit('renders a div element with the given src as background', () => {
-    expect(animated.find('div')).to.have.style('background-image', `url(${account.get('avatar')})`);
-  });
-
-  xit('renders a div element of the given size', () => {
-    ['width', 'height'].map((attr) => {
-      expect(animated.find('div')).to.have.style(attr, `${size}px`);
-    });
-  });
-
-  // Still
-  xit('renders a div element with the given static src as background if not autoplay', () => {
-    expect(still.find('div')).to.have.style('background-image', `url(${account.get('avatar_static')})`);
-  });
-
-  xit('renders a div element of the given size if not autoplay', () => {
-    ['width', 'height'].map((attr) => {
-      expect(still.find('div')).to.have.style(attr, `${size}px`);
-    });
-  });
-
-  // TODO add autoplay test if possible
-});
diff --git a/spec/javascript/components/avatar_overlay.test.js b/spec/javascript/components/avatar_overlay.test.js
deleted file mode 100644
index fe1d3a012..000000000
--- a/spec/javascript/components/avatar_overlay.test.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import AvatarOverlay from '../../../app/javascript/mastodon/components/avatar_overlay';
-
-import { expect } from 'chai';
-import { render } from 'enzyme';
-import { fromJS }  from 'immutable';
-
-describe('<Avatar />', () => {
-  const account = fromJS({
-    username: 'alice',
-    acct: 'alice',
-    display_name: 'Alice',
-    avatar: '/animated/alice.gif',
-    avatar_static: '/static/alice.jpg',
-  });
-
-  const friend = fromJS({
-    username: 'eve',
-    acct: 'eve@blackhat.lair',
-    display_name: 'Evelyn',
-    avatar: '/animated/eve.gif',
-    avatar_static: '/static/eve.jpg',
-  });
-
-  const overlay = render(<AvatarOverlay account={account} friend={friend} />);
-
-  xit('renders account static src as base of overlay avatar', () => {
-    expect(overlay.find('.account__avatar-overlay-base'))
-      .to.have.style('background-image', `url(${account.get('avatar_static')})`);
-  });
-
-  xit('renders friend static src as overlay of overlay avatar', () => {
-    expect(overlay.find('.account__avatar-overlay-overlay'))
-      .to.have.style('background-image', `url(${friend.get('avatar_static')})`);
-  });
-});
diff --git a/spec/javascript/components/button.test.js b/spec/javascript/components/button.test.js
deleted file mode 100644
index d2cd0b4e7..000000000
--- a/spec/javascript/components/button.test.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import Button from '../../../app/javascript/mastodon/components/button';
-
-import { expect } from 'chai';
-import { shallow } from 'enzyme';
-import sinon from 'sinon';
-
-describe('<Button />', () => {
-  xit('renders a button element', () => {
-    const wrapper = shallow(<Button />);
-    expect(wrapper).to.match('button');
-  });
-
-  xit('renders the given text', () => {
-    const text = 'foo';
-    const wrapper = shallow(<Button text={text} />);
-    expect(wrapper.find('button')).to.have.text(text);
-  });
-
-  it('handles click events using the given handler', () => {
-    const handler = sinon.spy();
-    const wrapper = shallow(<Button onClick={handler} />);
-    wrapper.find('button').simulate('click');
-    expect(handler.calledOnce).to.equal(true);
-  });
-
-  it('does not handle click events if props.disabled given', () => {
-    const handler = sinon.spy();
-    const wrapper = shallow(<Button onClick={handler} disabled />);
-    wrapper.find('button').simulate('click');
-    expect(handler.called).to.equal(false);
-  });
-
-  xit('renders a disabled attribute if props.disabled given', () => {
-    const wrapper = shallow(<Button disabled />);
-    expect(wrapper.find('button')).to.be.disabled();
-  });
-
-  xit('renders the children', () => {
-    const children = <p>children</p>;
-    const wrapper = shallow(<Button>{children}</Button>);
-    expect(wrapper.find('button')).to.contain(children);
-  });
-
-  xit('renders the props.text instead of children', () => {
-    const text = 'foo';
-    const children = <p>children</p>;
-    const wrapper = shallow(<Button text={text}>{children}</Button>);
-    expect(wrapper.find('button')).to.have.text(text);
-    expect(wrapper.find('button')).to.not.contain(children);
-  });
-
-  xit('renders style="display: block; width: 100%;" if props.block given', () => {
-    const wrapper = shallow(<Button block />);
-    expect(wrapper.find('button')).to.have.className('button--block');
-  });
-
-  xit('renders style="display: inline-block; width: auto;" by default', () => {
-    const wrapper = shallow(<Button />);
-    expect(wrapper.find('button')).to.not.have.className('button--block');
-  });
-
-  xit('adds class "button-secondary" if props.secondary given', () => {
-    const wrapper = shallow(<Button secondary />);
-    expect(wrapper.find('button')).to.have.className('button-secondary');
-  });
-
-  xit('does not add class "button-secondary" by default', () => {
-    const wrapper = shallow(<Button />);
-    expect(wrapper.find('button')).to.not.have.className('button-secondary');
-  });
-});
diff --git a/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js
deleted file mode 100644
index 97a111894..000000000
--- a/spec/javascript/components/display_name.test.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import DisplayName from '../../../app/javascript/mastodon/components/display_name';
-
-import { expect } from 'chai';
-import { render } from 'enzyme';
-import { fromJS }  from 'immutable';
-
-describe('<DisplayName />', () => {
-  xit('renders display name + account name', () => {
-    const account = fromJS({
-      username: 'bar',
-      acct: 'bar@baz',
-      display_name_html: '<p>Foo</p>',
-    });
-    const wrapper = render(<DisplayName account={account} />);
-    expect(wrapper).to.have.text('Foo @bar@baz');
-  });
-});
diff --git a/spec/javascript/components/emoji_index.test.js b/spec/javascript/components/emoji_index.test.js
deleted file mode 100644
index cdb50cb8c..000000000
--- a/spec/javascript/components/emoji_index.test.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import { expect } from 'chai';
-import { search } from '../../../app/javascript/mastodon/features/emoji/emoji_mart_search_light';
-import { emojiIndex } from 'emoji-mart';
-import { pick } from 'lodash';
-
-const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
-
-// hack to fix https://github.com/chaijs/type-detect/issues/98
-// see: https://github.com/chaijs/type-detect/issues/98#issuecomment-325010785
-import jsdom from 'jsdom';
-global.window = new jsdom.JSDOM().window;
-global.document = window.document;
-global.HTMLElement = window.HTMLElement;
-
-describe('emoji_index', () => {
-
-  it('should give same result for emoji_index_light and emoji-mart', () => {
-    let expected = [{
-      id: 'pineapple',
-      unified: '1f34d',
-      native: '๐Ÿ',
-    }];
-    expect(search('pineapple').map(trimEmojis)).to.deep.equal(expected);
-    expect(emojiIndex.search('pineapple').map(trimEmojis)).to.deep.equal(expected);
-  });
-
-  it('orders search results correctly', () => {
-    let expected = [{
-      id: 'apple',
-      unified: '1f34e',
-      native: '๐ŸŽ',
-    }, {
-      id: 'pineapple',
-      unified: '1f34d',
-      native: '๐Ÿ',
-    }, {
-      id: 'green_apple',
-      unified: '1f34f',
-      native: '๐Ÿ',
-    }, {
-      id: 'iphone',
-      unified: '1f4f1',
-      native: '๐Ÿ“ฑ',
-    }];
-    expect(search('apple').map(trimEmojis)).to.deep.equal(expected);
-    expect(emojiIndex.search('apple').map(trimEmojis)).to.deep.equal(expected);
-  });
-
-  it('handles custom emoji', () => {
-    let custom = [{
-      id: 'mastodon',
-      name: 'mastodon',
-      short_names: ['mastodon'],
-      text: '',
-      emoticons: [],
-      keywords: ['mastodon'],
-      imageUrl: 'http://example.com',
-      custom: true,
-    }];
-    search('', { custom });
-    emojiIndex.search('', { custom });
-    let expected = [ { id: 'mastodon', custom: true } ];
-    expect(search('masto').map(trimEmojis)).to.deep.equal(expected);
-    expect(emojiIndex.search('masto').map(trimEmojis)).to.deep.equal(expected);
-  });
-
-  it('should filter only emojis we care about, exclude pineapple', () => {
-    let emojisToShowFilter = (unified) => unified !== '1F34D';
-    expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
-      .not.to.contain('pineapple');
-    expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
-      .not.to.contain('pineapple');
-  });
-
-  it('can include/exclude categories', () => {
-    expect(search('flag', { include: ['people'] }))
-      .to.deep.equal([]);
-    expect(emojiIndex.search('flag', { include: ['people'] }))
-      .to.deep.equal([]);
-  });
-
-  it('does an emoji whose unified name is irregular', () => {
-    let expected = [{
-      'id': 'water_polo',
-      'unified': '1f93d',
-      'native': '๐Ÿคฝ',
-    }, {
-      'id': 'man-playing-water-polo',
-      'unified': '1f93d-200d-2642-fe0f',
-      'native': '๐Ÿคฝโ€โ™‚๏ธ',
-    }, {
-      'id': 'woman-playing-water-polo',
-      'unified': '1f93d-200d-2640-fe0f',
-      'native': '๐Ÿคฝโ€โ™€๏ธ',
-    }];
-    expect(search('polo').map(trimEmojis)).to.deep.equal(expected);
-    expect(emojiIndex.search('polo').map(trimEmojis)).to.deep.equal(expected);
-  });
-
-  it('can search for thinking_face', () => {
-    let expected = [ { id: 'thinking_face', unified: '1f914', native: '๐Ÿค”' } ];
-    expect(search('thinking_fac').map(trimEmojis)).to.deep.equal(expected);
-    expect(emojiIndex.search('thinking_fac').map(trimEmojis)).to.deep.equal(expected);
-  });
-
-  it('can search for woman-facepalming', () => {
-    let expected = [ { id: 'woman-facepalming', unified: '1f926-200d-2640-fe0f', native: '๐Ÿคฆโ€โ™€๏ธ' } ];
-    expect(search('woman-facep').map(trimEmojis)).to.deep.equal(expected);
-    expect(emojiIndex.search('woman-facep').map(trimEmojis)).deep.equal(expected);
-  });
-});
diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js
deleted file mode 100644
index 3105c8e3f..000000000
--- a/spec/javascript/components/emojify.test.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { expect } from 'chai';
-import emojify from '../../../app/javascript/mastodon/features/emoji/emoji';
-
-describe('emojify', () => {
-  it('ignores unknown shortcodes', () => {
-    expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:');
-  });
-
-  it('ignores shortcodes inside of tags', () => {
-    expect(emojify('<p data-foo=":smile:"></p>')).to.equal('<p data-foo=":smile:"></p>');
-  });
-
-  it('works with unclosed tags', () => {
-    expect(emojify('hello>')).to.equal('hello>');
-    expect(emojify('<hello')).to.equal('<hello');
-  });
-
-  it('works with unclosed shortcodes', () => {
-    expect(emojify('smile:')).to.equal('smile:');
-    expect(emojify(':smile')).to.equal(':smile');
-  });
-
-  it('does unicode', () => {
-    expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal(
-      '<img draggable="false" class="emojione" alt="๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
-    expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).to.equal(
-      '<img draggable="false" class="emojione" alt="๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
-    expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).to.equal('<img draggable="false" class="emojione" alt="๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
-    expect(emojify('\u2757')).to.equal(
-      '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" />');
-  });
-
-  it('does multiple unicode', () => {
-    expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal(
-      '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" />');
-    expect(emojify('\u2757#\uFE0F\u20E3')).to.equal(
-      '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" />');
-    expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal(
-      '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" />');
-    expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal(
-      'foo <img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
-  });
-
-  it('ignores unicode inside of tags', () => {
-    expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).to.equal('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
-  });
-
-  it('does multiple emoji properly (issue 5188)', () => {
-    expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).to.equal('<img draggable="false" class="emojione" alt="๐Ÿ‘Œ" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="๐ŸŒˆ" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="๐Ÿ’•" title=":two_hearts:" src="/emoji/1f495.svg" />');
-    expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).to.equal('<img draggable="false" class="emojione" alt="๐Ÿ‘Œ" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="๐ŸŒˆ" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="๐Ÿ’•" title=":two_hearts:" src="/emoji/1f495.svg" />');
-  });
-
-  it('does an emoji that has no shortcode', () => {
-    expect(emojify('๐Ÿ•‰๏ธ')).to.equal('<img draggable="false" class="emojione" alt="๐Ÿ•‰๏ธ" title="" src="/emoji/1f549.svg" />');
-  });
-
-  it('does an emoji whose filename is irregular', () => {
-    expect(emojify('โ†™๏ธ')).to.equal('<img draggable="false" class="emojione" alt="โ†™๏ธ" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
-  });
-
-});
diff --git a/spec/javascript/components/features/ui/components/column.test.js b/spec/javascript/components/features/ui/components/column.test.js
deleted file mode 100644
index 4491d6e19..000000000
--- a/spec/javascript/components/features/ui/components/column.test.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { expect } from 'chai';
-import { mount } from 'enzyme';
-import sinon from 'sinon';
-import React from 'react';
-import Column from '../../../../../../app/javascript/mastodon/features/ui/components/column';
-import ColumnHeader from '../../../../../../app/javascript/mastodon/features/ui/components/column_header';
-
-describe('<Column />', () => {
-  describe('<ColumnHeader /> click handler', () => {
-    beforeEach(() => {
-      global.requestAnimationFrame = sinon.spy();
-    });
-
-    it('runs the scroll animation if the column contains scrollable content', () => {
-      const wrapper = mount(
-        <Column heading='notifications'>
-          <div className='scrollable' />
-        </Column>
-      );
-      wrapper.find(ColumnHeader).simulate('click');
-      expect(global.requestAnimationFrame.called).to.equal(true);
-    });
-
-    it('does not try to scroll if there is no scrollable content', () => {
-      const wrapper = mount(<Column heading='notifications' />);
-      wrapper.find(ColumnHeader).simulate('click');
-      expect(global.requestAnimationFrame.called).to.equal(false);
-    });
-  });
-});
diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js
deleted file mode 100644
index ab8a36b95..000000000
--- a/spec/javascript/setup.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { JSDOM } from 'jsdom';
-import Enzyme from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-const { window } = new JSDOM('', {
-  userAgent: 'node.js',
-});
-
-Object.keys(window).forEach(property => {
-  if (typeof global[property] === 'undefined') {
-    global[property] = window[property];
-  }
-});
diff --git a/yarn.lock b/yarn.lock
index f32c9aceb..9e25522d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3,8 +3,8 @@
 
 
 "@types/node@^6.0.46":
-  version "6.0.80"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.80.tgz#914a75799605b4609bd9a2918c865ba3c4141367"
+  version "6.0.89"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.89.tgz#154be0e6a823760cd6083aa8c48f952e2e63e0b0"
 
 abab@^1.0.3:
   version "1.0.3"
@@ -114,6 +114,10 @@ ansi-escapes@^1.1.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
 
+ansi-escapes@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
+
 ansi-html@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
@@ -136,6 +140,12 @@ ansi-styles@^3.1.0:
   dependencies:
     color-convert "^1.0.0"
 
+ansi-styles@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+  dependencies:
+    color-convert "^1.9.0"
+
 any-promise@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27"
@@ -151,6 +161,12 @@ ap@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/ap/-/ap-0.2.0.tgz#ae0942600b29912f0d2b14ec60c45e8f330b6110"
 
+append-transform@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+  dependencies:
+    default-require-extensions "^1.0.0"
+
 aproba@^1.0.3:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1"
@@ -228,7 +244,7 @@ array.prototype.find@^2.0.1:
     define-properties "^1.1.2"
     es-abstract "^1.7.0"
 
-arrify@^1.0.0:
+arrify@^1.0.0, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
 
@@ -262,14 +278,14 @@ assert@^1.1.1, assert@^1.3.0:
   dependencies:
     util "0.10.3"
 
-assertion-error@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
-
 ast-types-flow@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
 
+astral-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+
 async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -282,11 +298,11 @@ async@0.2.x:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
-async@^1.5.2:
+async@^1.4.0, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
-async@^2.1.2, async@^2.1.5:
+async@^2.1.2, async@^2.1.4, async@^2.1.5:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
   dependencies:
@@ -353,7 +369,7 @@ babel-code-frame@^6.26.0:
     esutils "^2.0.2"
     js-tokens "^3.0.2"
 
-babel-core@^6.25.0, babel-core@^6.26.0:
+babel-core@^6.0.0, babel-core@^6.25.0, babel-core@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
   dependencies:
@@ -386,7 +402,7 @@ babel-eslint@^7.2.3:
     babel-types "^6.23.0"
     babylon "^6.17.0"
 
-babel-generator@^6.26.0:
+babel-generator@^6.18.0, babel-generator@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
   dependencies:
@@ -508,6 +524,13 @@ babel-helpers@^6.24.1:
     babel-runtime "^6.22.0"
     babel-template "^6.24.1"
 
+babel-jest@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-21.2.0.tgz#2ce059519a9374a2c46f2455b6fbef5ad75d863e"
+  dependencies:
+    babel-plugin-istanbul "^4.0.0"
+    babel-preset-jest "^21.2.0"
+
 babel-loader@^7.1.1:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126"
@@ -532,6 +555,18 @@ babel-plugin-check-es2015-constants@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
+babel-plugin-istanbul@^4.0.0:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
+  dependencies:
+    find-up "^2.1.0"
+    istanbul-lib-instrument "^1.7.5"
+    test-exclude "^4.1.1"
+
+babel-plugin-jest-hoist@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz#2cef637259bd4b628a6cace039de5fcd14dbb006"
+
 babel-plugin-lodash@^3.2.11:
   version "3.2.11"
   resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.2.11.tgz#21c8fdec9fe1835efaa737873e3902bdd66d5701"
@@ -591,7 +626,7 @@ babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
 
-babel-plugin-syntax-object-rest-spread@^6.8.0:
+babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
 
@@ -923,6 +958,13 @@ babel-preset-flow@^6.23.0:
   dependencies:
     babel-plugin-transform-flow-strip-types "^6.22.0"
 
+babel-preset-jest@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-21.2.0.tgz#ff9d2bce08abd98e8a36d9a8a5189b9173b85638"
+  dependencies:
+    babel-plugin-jest-hoist "^21.2.0"
+    babel-plugin-syntax-object-rest-spread "^6.13.0"
+
 babel-preset-react@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380"
@@ -960,6 +1002,16 @@ babel-runtime@^6.26.0:
     core-js "^2.4.0"
     regenerator-runtime "^0.11.0"
 
+babel-template@^6.16.0, babel-template@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+  dependencies:
+    babel-runtime "^6.26.0"
+    babel-traverse "^6.26.0"
+    babel-types "^6.26.0"
+    babylon "^6.18.0"
+    lodash "^4.17.4"
+
 babel-template@^6.24.1, babel-template@^6.3.0:
   version "6.25.0"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071"
@@ -970,14 +1022,18 @@ babel-template@^6.24.1, babel-template@^6.3.0:
     babylon "^6.17.2"
     lodash "^4.2.0"
 
-babel-template@^6.26.0:
+babel-traverse@^6.18.0, babel-traverse@^6.26.0:
   version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
   dependencies:
+    babel-code-frame "^6.26.0"
+    babel-messages "^6.23.0"
     babel-runtime "^6.26.0"
-    babel-traverse "^6.26.0"
     babel-types "^6.26.0"
     babylon "^6.18.0"
+    debug "^2.6.8"
+    globals "^9.18.0"
+    invariant "^2.2.2"
     lodash "^4.17.4"
 
 babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.25.0:
@@ -994,19 +1050,14 @@ babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.25.0:
     invariant "^2.2.0"
     lodash "^4.2.0"
 
-babel-traverse@^6.26.0:
+babel-types@^6.18.0, babel-types@^6.26.0:
   version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
   dependencies:
-    babel-code-frame "^6.26.0"
-    babel-messages "^6.23.0"
     babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    babylon "^6.18.0"
-    debug "^2.6.8"
-    globals "^9.18.0"
-    invariant "^2.2.2"
+    esutils "^2.0.2"
     lodash "^4.17.4"
+    to-fast-properties "^1.0.3"
 
 babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.25.0:
   version "6.25.0"
@@ -1017,15 +1068,6 @@ babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.25
     lodash "^4.2.0"
     to-fast-properties "^1.0.1"
 
-babel-types@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
-  dependencies:
-    babel-runtime "^6.26.0"
-    esutils "^2.0.2"
-    lodash "^4.17.4"
-    to-fast-properties "^1.0.3"
-
 babylon@^6.17.0, babylon@^6.17.2:
   version "6.17.4"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a"
@@ -1139,9 +1181,11 @@ brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
 
-browser-stdout@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
+browser-resolve@^1.11.2:
+  version "1.11.2"
+  resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
+  dependencies:
+    resolve "1.1.7"
 
 browserify-aes@^1.0.0, browserify-aes@^1.0.4:
   version "1.0.6"
@@ -1215,6 +1259,12 @@ browserslist@^2.4.0:
     caniuse-lite "^1.0.30000718"
     electron-to-chromium "^1.3.18"
 
+bser@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+  dependencies:
+    node-int64 "^0.4.0"
+
 buffer-indexof@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982"
@@ -1261,6 +1311,10 @@ callsites@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
 
+callsites@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+
 camelcase-css@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-1.0.1.tgz#157c4238265f5cf94a1dffde86446552cbf3f705"
@@ -1320,24 +1374,6 @@ center-align@^0.1.1:
     align-text "^0.1.3"
     lazy-cache "^1.0.3"
 
-chai-enzyme@^0.8.0:
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/chai-enzyme/-/chai-enzyme-0.8.0.tgz#609c552a1dcdb091f435e1e281cc4f2149a33be1"
-  dependencies:
-    html "^1.0.0"
-    react-element-to-jsx-string "^5.0.0"
-
-chai@^4.1.0:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c"
-  dependencies:
-    assertion-error "^1.0.1"
-    check-error "^1.0.1"
-    deep-eql "^3.0.0"
-    get-func-name "^2.0.0"
-    pathval "^1.0.0"
-    type-detect "^4.0.0"
-
 chain-function@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
@@ -1368,10 +1404,6 @@ chalk@^2.1.0:
     escape-string-regexp "^1.0.5"
     supports-color "^4.0.0"
 
-check-error@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
-
 cheerio@^1.0.0-rc.2:
   version "1.0.0-rc.2"
   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
@@ -1398,6 +1430,10 @@ chokidar@^1.6.0, chokidar@^1.7.0:
   optionalDependencies:
     fsevents "^1.0.0"
 
+ci-info@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a"
+
 cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@@ -1472,11 +1508,7 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
-collapse-white-space@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c"
-
-color-convert@^1.0.0, color-convert@^1.3.0:
+color-convert@^1.0.0, color-convert@^1.3.0, color-convert@^1.9.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
   dependencies:
@@ -1522,12 +1554,6 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@2.9.0:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
-  dependencies:
-    graceful-readlink ">= 1.0.0"
-
 commander@^2.8.1, commander@^2.9.0:
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
@@ -1571,7 +1597,7 @@ concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 
-concat-stream@^1.4.7, concat-stream@^1.5.2:
+concat-stream@^1.5.2:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
   dependencies:
@@ -1617,7 +1643,7 @@ convert-source-map@^0.3.3:
   version "0.3.5"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
 
-convert-source-map@^1.1.1, convert-source-map@^1.5.0:
+convert-source-map@^1.1.1, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
 
@@ -1930,7 +1956,7 @@ debug@2.6.8, debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.6, debug@^2.6.
   dependencies:
     ms "2.0.0"
 
-debug@2.6.9:
+debug@2.6.9, debug@^2.6.3:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -1948,12 +1974,6 @@ decimal.js@7.2.3:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-7.2.3.tgz#6434c3b8a8c375780062fc633d0d2bbdb264cc78"
 
-deep-eql@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
-  dependencies:
-    type-detect "^4.0.0"
-
 deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -1966,6 +1986,12 @@ deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
 
+default-require-extensions@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+  dependencies:
+    strip-bom "^2.0.0"
+
 defaults@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
@@ -2047,13 +2073,9 @@ detect-passive-events@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a"
 
-diff@3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
-
-diff@^3.1.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9"
+diff@^3.2.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
 
 diffie-hellman@^5.0.0:
   version "5.0.2"
@@ -2159,10 +2181,6 @@ ecc-jsbn@~0.1.1:
   dependencies:
     jsbn "~0.1.0"
 
-editions@^1.1.1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.3.tgz#0907101bdda20fac3cbe334c27cbd0688dc99a5b"
-
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2227,8 +2245,8 @@ entities@^1.1.1, entities@~1.1.1:
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
 enzyme-adapter-react-16@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.0.tgz#e7edd5536743818dcbef336d40d7da59b3a7db8e"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.1.tgz#066cb1735e65d8d95841a023f94dab3ce6109e17"
   dependencies:
     enzyme-adapter-utils "^1.0.0"
     lodash "^4.17.4"
@@ -2237,16 +2255,16 @@ enzyme-adapter-react-16@^1.0.0:
     prop-types "^15.5.10"
 
 enzyme-adapter-utils@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.0.0.tgz#e94eee63da9a798d498adb1162a2102ed04fc638"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.0.1.tgz#fcd81223339a55a312f7552641e045c404084009"
   dependencies:
     lodash "^4.17.4"
     object.assign "^4.0.4"
     prop-types "^15.5.10"
 
 enzyme@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.0.0.tgz#94ce364254dc654c4e619b25eecc644bf6481de7"
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.1.0.tgz#d8ca84085790fbcec6ed40badd14478faee4c25a"
   dependencies:
     cheerio "^1.0.0-rc.2"
     function.prototype.name "^1.0.3"
@@ -2257,9 +2275,9 @@ enzyme@^3.0.0:
     object.entries "^1.0.4"
     object.values "^1.0.4"
     raf "^3.3.2"
-    rst-selector-parser "^2.2.1"
+    rst-selector-parser "^2.2.2"
 
-errno@^0.1.3:
+errno@^0.1.3, errno@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
   dependencies:
@@ -2271,7 +2289,17 @@ error-ex@^1.2.0:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.6.1, es-abstract@^1.7.0:
+es-abstract@^1.6.1:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
+  dependencies:
+    es-to-primitive "^1.1.1"
+    function-bind "^1.1.1"
+    has "^1.0.1"
+    is-callable "^1.1.3"
+    is-regex "^1.0.4"
+
+es-abstract@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c"
   dependencies:
@@ -2344,7 +2372,7 @@ escape-html@^1.0.3, escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
 
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
@@ -2504,6 +2532,12 @@ evp_bytestokey@^1.0.0:
   dependencies:
     create-hash "^1.1.1"
 
+exec-sh@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
+  dependencies:
+    merge "^1.1.3"
+
 execa@^0.5.0:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36"
@@ -2532,6 +2566,17 @@ expand-range@^1.8.1:
   dependencies:
     fill-range "^2.1.0"
 
+expect@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-21.2.1.tgz#003ac2ac7005c3c29e73b38a272d4afadd6d1d7b"
+  dependencies:
+    ansi-styles "^3.2.0"
+    jest-diff "^21.2.1"
+    jest-get-type "^21.2.0"
+    jest-matcher-utils "^21.2.1"
+    jest-message-util "^21.2.1"
+    jest-regex-util "^21.2.0"
+
 express@^4.13.3:
   version "4.15.3"
   resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662"
@@ -2647,6 +2692,12 @@ faye-websocket@~0.11.0:
   dependencies:
     websocket-driver ">=0.5.1"
 
+fb-watchman@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+  dependencies:
+    bser "^2.0.0"
+
 fbjs@^0.8.14, fbjs@^0.8.16:
   version "0.8.16"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
@@ -2695,6 +2746,13 @@ filename-regex@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
 
+fileset@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
+  dependencies:
+    glob "^7.0.3"
+    minimatch "^3.0.3"
+
 filesize@^3.5.9:
   version "3.5.10"
   resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f"
@@ -2813,12 +2871,6 @@ form-data@~2.1.1:
     combined-stream "^1.0.5"
     mime-types "^2.1.12"
 
-formatio@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
-  dependencies:
-    samsam "1.x"
-
 forwarded@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
@@ -2853,7 +2905,7 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
-fsevents@*, fsevents@^1.0.0:
+fsevents@*, fsevents@^1.0.0, fsevents@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
   dependencies:
@@ -2877,9 +2929,9 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
     mkdirp ">=0.5 0"
     rimraf "2"
 
-function-bind@^1.0.2, function-bind@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 
 function.prototype.name@^1.0.3:
   version "1.0.3"
@@ -2926,10 +2978,6 @@ get-caller-file@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
 
-get-func-name@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
-
 get-stdin@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -2960,17 +3008,6 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob@7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.2"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@@ -3021,17 +3058,13 @@ gonzales-pe@^4.0.3:
   dependencies:
     minimist "1.1.x"
 
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
-"graceful-readlink@>= 1.0.0":
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
-
-growl@1.9.2:
-  version "1.9.2"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
+growly@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
 
 gzip-size@^3.0.0:
   version "3.0.0"
@@ -3043,6 +3076,16 @@ handle-thing@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
 
+handlebars@^4.0.3:
+  version "4.0.10"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
+  dependencies:
+    async "^1.4.0"
+    optimist "^0.6.1"
+    source-map "^0.4.4"
+  optionalDependencies:
+    uglify-js "^2.6"
+
 har-schema@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
@@ -3100,10 +3143,6 @@ hawk@~3.1.3:
     hoek "2.x.x"
     sntp "1.x.x"
 
-he@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
-
 history@^4.7.2:
   version "4.7.2"
   resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b"
@@ -3164,12 +3203,6 @@ html-entities@^1.2.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
 
-html@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/html/-/html-1.0.0.tgz#a544fa9ea5492bfb3a2cca8210a10be7b5af1f61"
-  dependencies:
-    concat-stream "^1.4.7"
-
 htmlparser2@^3.9.1:
   version "3.9.2"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -3426,6 +3459,12 @@ is-callable@^1.1.1, is-callable@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
 
+is-ci@^1.0.10:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e"
+  dependencies:
+    ci-info "^1.0.0"
+
 is-date-object@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
@@ -3549,16 +3588,12 @@ is-property@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
 
-is-regex@^1.0.3:
+is-regex@^1.0.3, is-regex@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
   dependencies:
     has "^1.0.1"
 
-is-regexp@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
-
 is-resolvable@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
@@ -3632,10 +3667,298 @@ isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
+istanbul-api@^1.1.1:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680"
+  dependencies:
+    async "^2.1.4"
+    fileset "^2.0.2"
+    istanbul-lib-coverage "^1.1.1"
+    istanbul-lib-hook "^1.0.7"
+    istanbul-lib-instrument "^1.8.0"
+    istanbul-lib-report "^1.1.1"
+    istanbul-lib-source-maps "^1.2.1"
+    istanbul-reports "^1.1.2"
+    js-yaml "^3.7.0"
+    mkdirp "^0.5.1"
+    once "^1.4.0"
+
+istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+
+istanbul-lib-hook@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc"
+  dependencies:
+    append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.8.0.tgz#66f6c9421cc9ec4704f76f2db084ba9078a2b532"
+  dependencies:
+    babel-generator "^6.18.0"
+    babel-template "^6.16.0"
+    babel-traverse "^6.18.0"
+    babel-types "^6.18.0"
+    babylon "^6.18.0"
+    istanbul-lib-coverage "^1.1.1"
+    semver "^5.3.0"
+
+istanbul-lib-report@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9"
+  dependencies:
+    istanbul-lib-coverage "^1.1.1"
+    mkdirp "^0.5.1"
+    path-parse "^1.0.5"
+    supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c"
+  dependencies:
+    debug "^2.6.3"
+    istanbul-lib-coverage "^1.1.1"
+    mkdirp "^0.5.1"
+    rimraf "^2.6.1"
+    source-map "^0.5.3"
+
+istanbul-reports@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.2.tgz#0fb2e3f6aa9922bd3ce45d05d8ab4d5e8e07bd4f"
+  dependencies:
+    handlebars "^4.0.3"
+
 javascript-natural-sort@0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
 
+jest-changed-files@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29"
+  dependencies:
+    throat "^4.0.0"
+
+jest-cli@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-21.2.1.tgz#9c528b6629d651911138d228bdb033c157ec8c00"
+  dependencies:
+    ansi-escapes "^3.0.0"
+    chalk "^2.0.1"
+    glob "^7.1.2"
+    graceful-fs "^4.1.11"
+    is-ci "^1.0.10"
+    istanbul-api "^1.1.1"
+    istanbul-lib-coverage "^1.0.1"
+    istanbul-lib-instrument "^1.4.2"
+    istanbul-lib-source-maps "^1.1.0"
+    jest-changed-files "^21.2.0"
+    jest-config "^21.2.1"
+    jest-environment-jsdom "^21.2.1"
+    jest-haste-map "^21.2.0"
+    jest-message-util "^21.2.1"
+    jest-regex-util "^21.2.0"
+    jest-resolve-dependencies "^21.2.0"
+    jest-runner "^21.2.1"
+    jest-runtime "^21.2.1"
+    jest-snapshot "^21.2.1"
+    jest-util "^21.2.1"
+    micromatch "^2.3.11"
+    node-notifier "^5.0.2"
+    pify "^3.0.0"
+    slash "^1.0.0"
+    string-length "^2.0.0"
+    strip-ansi "^4.0.0"
+    which "^1.2.12"
+    worker-farm "^1.3.1"
+    yargs "^9.0.0"
+
+jest-config@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-21.2.1.tgz#c7586c79ead0bcc1f38c401e55f964f13bf2a480"
+  dependencies:
+    chalk "^2.0.1"
+    glob "^7.1.1"
+    jest-environment-jsdom "^21.2.1"
+    jest-environment-node "^21.2.1"
+    jest-get-type "^21.2.0"
+    jest-jasmine2 "^21.2.1"
+    jest-regex-util "^21.2.0"
+    jest-resolve "^21.2.0"
+    jest-util "^21.2.1"
+    jest-validate "^21.2.1"
+    pretty-format "^21.2.1"
+
+jest-diff@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.2.1.tgz#46cccb6cab2d02ce98bc314011764bb95b065b4f"
+  dependencies:
+    chalk "^2.0.1"
+    diff "^3.2.0"
+    jest-get-type "^21.2.0"
+    pretty-format "^21.2.1"
+
+jest-docblock@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
+
+jest-environment-jsdom@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-21.2.1.tgz#38d9980c8259b2a608ec232deee6289a60d9d5b4"
+  dependencies:
+    jest-mock "^21.2.0"
+    jest-util "^21.2.1"
+    jsdom "^9.12.0"
+
+jest-environment-node@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-21.2.1.tgz#98c67df5663c7fbe20f6e792ac2272c740d3b8c8"
+  dependencies:
+    jest-mock "^21.2.0"
+    jest-util "^21.2.1"
+
+jest-get-type@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23"
+
+jest-haste-map@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-21.2.0.tgz#1363f0a8bb4338f24f001806571eff7a4b2ff3d8"
+  dependencies:
+    fb-watchman "^2.0.0"
+    graceful-fs "^4.1.11"
+    jest-docblock "^21.2.0"
+    micromatch "^2.3.11"
+    sane "^2.0.0"
+    worker-farm "^1.3.1"
+
+jest-jasmine2@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-21.2.1.tgz#9cc6fc108accfa97efebce10c4308548a4ea7592"
+  dependencies:
+    chalk "^2.0.1"
+    expect "^21.2.1"
+    graceful-fs "^4.1.11"
+    jest-diff "^21.2.1"
+    jest-matcher-utils "^21.2.1"
+    jest-message-util "^21.2.1"
+    jest-snapshot "^21.2.1"
+    p-cancelable "^0.3.0"
+
+jest-matcher-utils@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64"
+  dependencies:
+    chalk "^2.0.1"
+    jest-get-type "^21.2.0"
+    pretty-format "^21.2.1"
+
+jest-message-util@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.2.1.tgz#bfe5d4692c84c827d1dcf41823795558f0a1acbe"
+  dependencies:
+    chalk "^2.0.1"
+    micromatch "^2.3.11"
+    slash "^1.0.0"
+
+jest-mock@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-21.2.0.tgz#7eb0770e7317968165f61ea2a7281131534b3c0f"
+
+jest-regex-util@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.2.0.tgz#1b1e33e63143babc3e0f2e6c9b5ba1eb34b2d530"
+
+jest-resolve-dependencies@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-21.2.0.tgz#9e231e371e1a736a1ad4e4b9a843bc72bfe03d09"
+  dependencies:
+    jest-regex-util "^21.2.0"
+
+jest-resolve@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-21.2.0.tgz#068913ad2ba6a20218e5fd32471f3874005de3a6"
+  dependencies:
+    browser-resolve "^1.11.2"
+    chalk "^2.0.1"
+    is-builtin-module "^1.0.0"
+
+jest-runner@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-21.2.1.tgz#194732e3e518bfb3d7cbfc0fd5871246c7e1a467"
+  dependencies:
+    jest-config "^21.2.1"
+    jest-docblock "^21.2.0"
+    jest-haste-map "^21.2.0"
+    jest-jasmine2 "^21.2.1"
+    jest-message-util "^21.2.1"
+    jest-runtime "^21.2.1"
+    jest-util "^21.2.1"
+    pify "^3.0.0"
+    throat "^4.0.0"
+    worker-farm "^1.3.1"
+
+jest-runtime@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-21.2.1.tgz#99dce15309c670442eee2ebe1ff53a3cbdbbb73e"
+  dependencies:
+    babel-core "^6.0.0"
+    babel-jest "^21.2.0"
+    babel-plugin-istanbul "^4.0.0"
+    chalk "^2.0.1"
+    convert-source-map "^1.4.0"
+    graceful-fs "^4.1.11"
+    jest-config "^21.2.1"
+    jest-haste-map "^21.2.0"
+    jest-regex-util "^21.2.0"
+    jest-resolve "^21.2.0"
+    jest-util "^21.2.1"
+    json-stable-stringify "^1.0.1"
+    micromatch "^2.3.11"
+    slash "^1.0.0"
+    strip-bom "3.0.0"
+    write-file-atomic "^2.1.0"
+    yargs "^9.0.0"
+
+jest-snapshot@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-21.2.1.tgz#29e49f16202416e47343e757e5eff948c07fd7b0"
+  dependencies:
+    chalk "^2.0.1"
+    jest-diff "^21.2.1"
+    jest-matcher-utils "^21.2.1"
+    mkdirp "^0.5.1"
+    natural-compare "^1.4.0"
+    pretty-format "^21.2.1"
+
+jest-util@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.2.1.tgz#a274b2f726b0897494d694a6c3d6a61ab819bb78"
+  dependencies:
+    callsites "^2.0.0"
+    chalk "^2.0.1"
+    graceful-fs "^4.1.11"
+    jest-message-util "^21.2.1"
+    jest-mock "^21.2.0"
+    jest-validate "^21.2.1"
+    mkdirp "^0.5.1"
+
+jest-validate@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7"
+  dependencies:
+    chalk "^2.0.1"
+    jest-get-type "^21.2.0"
+    leven "^2.1.0"
+    pretty-format "^21.2.1"
+
+jest@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-21.2.1.tgz#c964e0b47383768a1438e3ccf3c3d470327604e1"
+  dependencies:
+    jest-cli "^21.2.1"
+
 js-base64@^2.1.8, js-base64@^2.1.9:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
@@ -3655,7 +3978,7 @@ js-yaml@^3.4.3, js-yaml@^3.5.1:
     argparse "^1.0.7"
     esprima "^4.0.0"
 
-js-yaml@^3.9.0:
+js-yaml@^3.7.0, js-yaml@^3.9.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
   dependencies:
@@ -3673,9 +3996,9 @@ jsbn@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
 
-jsdom@^11.1.0:
-  version "11.2.0"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.2.0.tgz#4f6b8736af3357c3af7227a3b54a5bda1c513fd6"
+jsdom@^9.12.0:
+  version "9.12.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4"
   dependencies:
     abab "^1.0.3"
     acorn "^4.0.4"
@@ -3686,17 +4009,15 @@ jsdom@^11.1.0:
     cssstyle ">= 0.2.37 < 0.3.0"
     escodegen "^1.6.1"
     html-encoding-sniffer "^1.0.1"
-    nwmatcher "^1.4.1"
-    parse5 "^3.0.2"
-    pn "^1.0.0"
+    nwmatcher ">= 1.3.9 < 2.0.0"
+    parse5 "^1.5.1"
     request "^2.79.0"
-    request-promise-native "^1.0.3"
     sax "^1.2.1"
     symbol-tree "^3.2.1"
     tough-cookie "^2.3.2"
     webidl-conversions "^4.0.0"
     whatwg-encoding "^1.0.1"
-    whatwg-url "^6.1.0"
+    whatwg-url "^4.3.0"
     xml-name-validator "^2.0.1"
 
 jsesc@^1.3.0:
@@ -3729,7 +4050,7 @@ json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
 
-json3@3.3.2, json3@^3.3.2:
+json3@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
 
@@ -3806,6 +4127,10 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
+leven@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
 levn@^0.3.0, levn@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -3875,10 +4200,6 @@ lodash._basecopy@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
 
-lodash._basecreate@^3.0.0:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821"
-
 lodash._bindcallback@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
@@ -3919,14 +4240,6 @@ lodash.clonedeep@^4.3.2:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
 
-lodash.create@3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
-  dependencies:
-    lodash._baseassign "^3.0.0"
-    lodash._basecreate "^3.0.0"
-    lodash._isiterateecall "^3.0.0"
-
 lodash.defaults@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c"
@@ -3970,10 +4283,6 @@ lodash.restparam@^3.0.0:
   version "3.6.1"
   resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
 
-lodash.sortby@^4.7.0:
-  version "4.7.0"
-  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
-
 lodash.tail@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
@@ -3990,10 +4299,6 @@ loglevel@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
 
-lolex@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
-
 longest@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -4028,6 +4333,12 @@ make-dir@^1.0.0:
   dependencies:
     pify "^2.3.0"
 
+makeerror@1.0.x:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+  dependencies:
+    tmpl "1.0.x"
+
 map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
@@ -4092,6 +4403,10 @@ merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
 
+merge@^1.1.3:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
+
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -4183,10 +4498,14 @@ minimist@1.1.x:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
 
-minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
+minimist@~0.0.1:
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
 mixin-object@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
@@ -4194,29 +4513,12 @@ mixin-object@^2.0.1:
     for-in "^0.1.3"
     is-extendable "^0.1.1"
 
-mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
     minimist "0.0.8"
 
-mocha@^3.4.1:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
-  dependencies:
-    browser-stdout "1.3.0"
-    commander "2.9.0"
-    debug "2.6.8"
-    diff "3.2.0"
-    escape-string-regexp "1.0.5"
-    glob "7.1.1"
-    growl "1.9.2"
-    he "1.1.1"
-    json3 "3.3.2"
-    lodash.create "3.1.1"
-    mkdirp "0.5.1"
-    supports-color "3.1.2"
-
 mousetrap@^1.5.2:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
@@ -4244,10 +4546,6 @@ nan@^2.0.0, nan@^2.3.0, nan@^2.3.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
 
-native-promise-only@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
-
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -4293,6 +4591,10 @@ node-gyp@^3.3.1:
     tar "^2.0.0"
     which "1"
 
+node-int64@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
 node-libs-browser@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
@@ -4321,6 +4623,15 @@ node-libs-browser@^2.0.0:
     util "^0.10.3"
     vm-browserify "0.0.4"
 
+node-notifier@^5.0.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff"
+  dependencies:
+    growly "^1.3.0"
+    semver "^5.3.0"
+    shellwords "^0.1.0"
+    which "^1.2.12"
+
 node-pre-gyp@^0.6.36, node-pre-gyp@^0.6.4:
   version "0.6.36"
   resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
@@ -4444,9 +4755,9 @@ number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
 
-nwmatcher@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.1.tgz#7ae9b07b0ea804db7e25f05cb5fe4097d4e4949f"
+"nwmatcher@>= 1.3.9 < 2.0.0":
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c"
 
 oauth-sign@~0.8.1:
   version "0.8.2"
@@ -4533,7 +4844,7 @@ on-headers@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
 
-once@^1.3.0, once@^1.3.3:
+once@^1.3.0, once@^1.3.3, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   dependencies:
@@ -4553,6 +4864,13 @@ opn@^5.1.0:
   dependencies:
     is-wsl "^1.1.0"
 
+optimist@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+  dependencies:
+    minimist "~0.0.1"
+    wordwrap "~0.0.2"
+
 optionator@^0.8.1, optionator@^0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
@@ -4603,6 +4921,10 @@ osenv@0, osenv@^0.1.4:
     os-homedir "^1.0.0"
     os-tmpdir "^1.0.0"
 
+p-cancelable@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
+
 p-finally@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -4668,7 +4990,11 @@ parse-json@^2.2.0:
   dependencies:
     error-ex "^1.2.0"
 
-parse5@^3.0.1, parse5@^3.0.2:
+parse5@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
+
+parse5@^3.0.1:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510"
   dependencies:
@@ -4740,10 +5066,6 @@ path-type@^2.0.0:
   dependencies:
     pify "^2.0.0"
 
-pathval@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
-
 pbkdf2@^3.0.3:
   version "3.0.12"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2"
@@ -4830,10 +5152,6 @@ pluralize@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
 
-pn@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9"
-
 portfinder@^1.0.9:
   version "1.0.13"
   resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
@@ -5348,6 +5666,13 @@ preserve@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
 
+pretty-format@^21.2.1:
+  version "21.2.1"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36"
+  dependencies:
+    ansi-regex "^3.0.0"
+    ansi-styles "^3.2.0"
+
 private@^0.1.6, private@^0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -5480,12 +5805,18 @@ quote@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01"
 
-raf@^3.1.0, raf@^3.3.2:
+raf@^3.1.0:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27"
   dependencies:
     performance-now "^2.1.0"
 
+raf@^3.3.2, raf@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
+  dependencies:
+    performance-now "^2.1.0"
+
 railroad-diagrams@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
@@ -5545,17 +5876,6 @@ react-dom@^16.0.0:
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
-react-element-to-jsx-string@^5.0.0:
-  version "5.0.7"
-  resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-5.0.7.tgz#c663a4800a9c712115c0d8519cb0215a46a1f0f2"
-  dependencies:
-    collapse-white-space "^1.0.0"
-    is-plain-object "^2.0.1"
-    lodash "^4.17.4"
-    sortobject "^1.0.0"
-    stringify-object "2.4.0"
-    traverse "^0.6.6"
-
 react-event-listener@^0.5.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.5.0.tgz#d82105135573e187e3d900d18150a5882304b8d1"
@@ -5941,20 +6261,6 @@ repeating@^2.0.0:
   dependencies:
     is-finite "^1.0.0"
 
-request-promise-core@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
-  dependencies:
-    lodash "^4.13.1"
-
-request-promise-native@^1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.4.tgz#86988ec8eee408e45579fce83bfd05b3adf9a155"
-  dependencies:
-    request-promise-core "1.1.1"
-    stealthy-require "^1.1.0"
-    tough-cookie ">=2.3.0"
-
 request@2, request@^2.79.0, request@^2.81.0:
   version "2.81.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
@@ -6039,6 +6345,10 @@ resolve-url@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
 
+resolve@1.1.7:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
 resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"
@@ -6096,7 +6406,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^2.0.0"
     inherits "^2.0.1"
 
-rst-selector-parser@^2.2.1:
+rst-selector-parser@^2.2.2:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.2.tgz#9927b619bd5af8dc23a76c64caef04edf90d2c65"
   dependencies:
@@ -6121,9 +6431,19 @@ safe-buffer@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
 
-samsam@1.x, samsam@^1.1.3:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67"
+sane@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56"
+  dependencies:
+    anymatch "^1.3.0"
+    exec-sh "^0.2.0"
+    fb-watchman "^2.0.0"
+    minimatch "^3.0.2"
+    minimist "^1.1.1"
+    walker "~1.0.5"
+    watch "~0.18.0"
+  optionalDependencies:
+    fsevents "^1.1.1"
 
 sass-graph@^2.1.1:
   version "2.2.4"
@@ -6309,23 +6629,14 @@ shelljs@^0.7.5:
     interpret "^1.0.0"
     rechoir "^0.6.2"
 
-signal-exit@^3.0.0:
+shellwords@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 
-sinon@^2.3.7:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36"
-  dependencies:
-    diff "^3.1.0"
-    formatio "1.2.0"
-    lolex "^1.6.0"
-    native-promise-only "^0.8.1"
-    path-to-regexp "^1.7.0"
-    samsam "^1.1.3"
-    text-encoding "0.6.4"
-    type-detect "^4.0.0"
-
 slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -6364,12 +6675,6 @@ sort-keys@^1.0.0:
   dependencies:
     is-plain-obj "^1.0.0"
 
-sortobject@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/sortobject/-/sortobject-1.1.1.tgz#4f695d4d44ed0a4c06482c34c2582a2dcdc2ab34"
-  dependencies:
-    editions "^1.1.1"
-
 source-list-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
@@ -6403,7 +6708,7 @@ source-map@^0.1.38:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.4.2:
+source-map@^0.4.2, source-map@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
   dependencies:
@@ -6494,10 +6799,6 @@ stdout-stream@^1.4.0:
   dependencies:
     readable-stream "^2.0.1"
 
-stealthy-require@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
-
 stream-browserify@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
@@ -6519,6 +6820,13 @@ strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
 
+string-length@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+  dependencies:
+    astral-regex "^1.0.0"
+    strip-ansi "^4.0.0"
+
 string-width@^1.0.1, string-width@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -6544,13 +6852,6 @@ string_decoder@~1.0.3:
   dependencies:
     safe-buffer "~5.1.0"
 
-stringify-object@2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-2.4.0.tgz#c62d11023eb21fe2d9b087be039a26df3b22a09d"
-  dependencies:
-    is-plain-obj "^1.0.0"
-    is-regexp "^1.0.0"
-
 stringstream@~0.0.4:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@@ -6571,16 +6872,16 @@ strip-ansi@^4.0.0:
   dependencies:
     ansi-regex "^3.0.0"
 
+strip-bom@3.0.0, strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
 strip-bom@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
   dependencies:
     is-utf8 "^0.2.0"
 
-strip-bom@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
-
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -6612,17 +6913,11 @@ sugarss@^1.0.0:
   dependencies:
     postcss "^6.0.0"
 
-supports-color@3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
-  dependencies:
-    has-flag "^1.0.0"
-
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
 
-supports-color@^3.2.3:
+supports-color@^3.1.2, supports-color@^3.2.3:
   version "3.2.3"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
   dependencies:
@@ -6706,14 +7001,24 @@ tcomb@^2.5.0, tcomb@^2.5.1:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0"
 
-text-encoding@0.6.4:
-  version "0.6.4"
-  resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
+test-exclude@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
+  dependencies:
+    arrify "^1.0.1"
+    micromatch "^2.3.11"
+    object-assign "^4.1.0"
+    read-pkg-up "^1.0.1"
+    require-main-filename "^1.0.1"
 
 text-table@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
 
+throat@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+
 throng@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/throng/-/throng-4.0.0.tgz#983c6ba1993b58eae859998aa687ffe88df84c17"
@@ -6742,6 +7047,10 @@ tiny-queue@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046"
 
+tmpl@1.0.x:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
 to-arraybuffer@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
@@ -6750,7 +7059,7 @@ to-fast-properties@^1.0.1, to-fast-properties@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
 
-tough-cookie@>=2.3.0, tough-cookie@^2.3.2, tough-cookie@~2.3.0:
+tough-cookie@^2.3.2, tough-cookie@~2.3.0:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
   dependencies:
@@ -6760,10 +7069,6 @@ tr46@~0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
 
-traverse@^0.6.6:
-  version "0.6.6"
-  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
-
 trim-newlines@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -6796,10 +7101,6 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-detect@^4.0.0:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea"
-
 type-is@~1.6.15:
   version "1.6.15"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
@@ -6819,7 +7120,7 @@ ua-parser-js@^0.7.9:
   version "0.7.13"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.13.tgz#cd9dd2f86493b3f44dbeeef3780fda74c5ee14be"
 
-uglify-js@^2.8.29:
+uglify-js@^2.6, uglify-js@^2.8.29:
   version "2.8.29"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
   dependencies:
@@ -6970,12 +7271,25 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
+walker@~1.0.5:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+  dependencies:
+    makeerror "1.0.x"
+
 warning@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
   dependencies:
     loose-envify "^1.0.0"
 
+watch@~0.18.0:
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"
+  dependencies:
+    exec-sh "^0.2.0"
+    minimist "^1.2.0"
+
 watchpack@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
@@ -6990,7 +7304,11 @@ wbuf@^1.1.0, wbuf@^1.7.2:
   dependencies:
     minimalistic-assert "^1.0.0"
 
-webidl-conversions@^4.0.0, webidl-conversions@^4.0.1:
+webidl-conversions@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+
+webidl-conversions@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0"
 
@@ -7128,13 +7446,12 @@ whatwg-fetch@>=0.10.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
 
-whatwg-url@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.1.0.tgz#5fc8279b93d75483b9ced8b26239854847a18578"
+whatwg-url@^4.3.0:
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"
   dependencies:
-    lodash.sortby "^4.7.0"
     tr46 "~0.0.3"
-    webidl-conversions "^4.0.1"
+    webidl-conversions "^3.0.0"
 
 whet.extend@~0.9.9:
   version "0.9.9"
@@ -7154,6 +7471,12 @@ which@1, which@^1.2.9:
   dependencies:
     isexe "^2.0.0"
 
+which@^1.2.12:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+  dependencies:
+    isexe "^2.0.0"
+
 wide-align@^1.1.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
@@ -7168,10 +7491,21 @@ wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
 
+wordwrap@~0.0.2:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
 wordwrap@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
 
+worker-farm@^1.3.1:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d"
+  dependencies:
+    errno "^0.1.4"
+    xtend "^4.0.1"
+
 wrap-ansi@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
@@ -7183,6 +7517,14 @@ wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 
+write-file-atomic@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
+  dependencies:
+    graceful-fs "^4.1.11"
+    imurmurhash "^0.1.4"
+    signal-exit "^3.0.2"
+
 write@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
@@ -7200,7 +7542,7 @@ xml-name-validator@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
 
-xtend@^4.0.0:
+xtend@^4.0.0, xtend@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 
@@ -7284,6 +7626,24 @@ yargs@^8.0.2:
     y18n "^3.2.1"
     yargs-parser "^7.0.0"
 
+yargs@^9.0.0:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c"
+  dependencies:
+    camelcase "^4.1.0"
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    read-pkg-up "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^7.0.0"
+
 yargs@~3.10.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
-- 
cgit 


From 981e20b03a67ecc01826ef32c5fc9fd28ed2917b Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Mon, 16 Oct 2017 00:33:50 -0700
Subject: Fix offline-plugin warning in dev mode (#5411)

---
 app/javascript/mastodon/main.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index a7fc22a00..b418ab2f2 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -1,4 +1,3 @@
-import * as OfflinePluginRuntime from 'offline-plugin/runtime';
 import * as WebPushSubscription from './web_push_subscription';
 import Mastodon from 'mastodon/containers/mastodon';
 import React from 'react';
@@ -25,7 +24,7 @@ function main() {
     ReactDOM.render(<Mastodon {...props} />, mountNode);
     if (process.env.NODE_ENV === 'production') {
       // avoid offline in dev mode because it's harder to debug
-      OfflinePluginRuntime.install();
+      require('offline-plugin/runtime').install();
       WebPushSubscription.register();
     }
     perf.stop('main()');
-- 
cgit 


From fa0be3f834b54bb276eb5233195181fa3760710f Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Mon, 16 Oct 2017 00:36:15 -0700
Subject: Add option to reduce motion (#5393)

* Add option to reduce motion

* Use HOC to wrap all Motion calls

* fix case-sensitive issue

* Avoid updating too frequently

* Get rid of unnecessary change to _simple_status.html.haml
---
 app/controllers/settings/preferences_controller.rb |  1 +
 app/javascript/mastodon/components/collapsable.js  |  2 +-
 .../mastodon/components/dropdown_menu.js           |  2 +-
 app/javascript/mastodon/components/icon_button.js  |  2 +-
 .../mastodon/features/account/components/header.js |  2 +-
 .../compose/components/privacy_dropdown.js         |  2 +-
 .../mastodon/features/compose/components/search.js |  2 +-
 .../mastodon/features/compose/components/upload.js |  2 +-
 .../features/compose/components/upload_progress.js |  2 +-
 .../features/compose/components/warning.js         |  2 +-
 .../containers/sensitive_button_container.js       |  2 +-
 app/javascript/mastodon/features/compose/index.js  |  2 +-
 .../mastodon/features/ui/components/upload_area.js |  2 +-
 .../mastodon/features/ui/util/optional_motion.js   | 34 ++++++++++++++++++++++
 app/lib/user_settings_decorator.rb                 |  5 ++++
 app/models/user.rb                                 |  4 +++
 app/serializers/initial_state_serializer.rb        |  1 +
 app/views/settings/preferences/show.html.haml      |  1 +
 .../stream_entries/_detailed_status.html.haml      |  2 +-
 config/locales/simple_form.en.yml                  |  1 +
 config/settings.yml                                |  1 +
 21 files changed, 61 insertions(+), 13 deletions(-)
 create mode 100644 app/javascript/mastodon/features/ui/util/optional_motion.js

(limited to 'app')

diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 207c7b324..069026715 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController
       :setting_boost_modal,
       :setting_delete_modal,
       :setting_auto_play_gif,
+      :setting_reduce_motion,
       :setting_system_font_ui,
       :setting_noindex,
       :setting_theme,
diff --git a/app/javascript/mastodon/components/collapsable.js b/app/javascript/mastodon/components/collapsable.js
index ad1453589..42ea37ec2 100644
--- a/app/javascript/mastodon/components/collapsable.js
+++ b/app/javascript/mastodon/components/collapsable.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 73ad46bb7..3a3ebf487 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import IconButton from './icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index 68d1a2735..d8e445cef 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 9ee7a56d9..07a6c5dec 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import IconButton from '../../../components/icon_button';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { connect } from 'react-redux';
 import ImmutablePureComponent from 'react-immutable-pure-component';
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index e38ed38c1..c1e85aee3 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { injectIntl, defineMessages } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 import classNames from 'classnames';
diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js
index f57d54618..398fc44ce 100644
--- a/app/javascript/mastodon/features/compose/components/search.js
+++ b/app/javascript/mastodon/features/compose/components/search.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 const messages = defineMessages({
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index cd9e08360..5d8d66cf7 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -2,7 +2,7 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import IconButton from '../../../components/icon_button';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { defineMessages, injectIntl } from 'react-intl';
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js
index 3e49098c7..d5e6f19cd 100644
--- a/app/javascript/mastodon/features/compose/components/upload_progress.js
+++ b/app/javascript/mastodon/features/compose/components/upload_progress.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/mastodon/features/compose/components/warning.js b/app/javascript/mastodon/features/compose/components/warning.js
index a0814e984..803b7f86a 100644
--- a/app/javascript/mastodon/features/compose/components/warning.js
+++ b/app/javascript/mastodon/features/compose/components/warning.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 export default class Warning extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
index 8624849f3..e4bd5a743 100644
--- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import IconButton from '../../../components/icon_button';
 import { changeComposeSensitivity } from '../../../actions/compose';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { injectIntl, defineMessages } from 'react-intl';
 
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 6166fce3c..0c66585c9 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -8,7 +8,7 @@ import { mountCompose, unmountCompose } from '../../actions/compose';
 import { Link } from 'react-router-dom';
 import { injectIntl, defineMessages } from 'react-intl';
 import SearchContainer from './containers/search_container';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import SearchResultsContainer from './containers/search_results_container';
 import { changeComposing } from '../../actions/compose';
diff --git a/app/javascript/mastodon/features/ui/components/upload_area.js b/app/javascript/mastodon/features/ui/components/upload_area.js
index dda28feeb..c19065be6 100644
--- a/app/javascript/mastodon/features/ui/components/upload_area.js
+++ b/app/javascript/mastodon/features/ui/components/upload_area.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'react-motion/lib/Motion';
+import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js
new file mode 100644
index 000000000..4276eeaa4
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/optional_motion.js
@@ -0,0 +1,34 @@
+// Like react-motion's Motion, but checks to see if the user prefers
+// reduced motion and uses a cross-fade in those cases.
+
+import Motion from 'react-motion/lib/Motion';
+import { connect } from 'react-redux';
+
+const stylesToKeep = ['opacity', 'backgroundOpacity'];
+
+const extractValue = (value) => {
+  // This is either an object with a "val" property or it's a number
+  return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
+};
+
+const mapStateToProps = (state, ownProps) => {
+  const reduceMotion = state.getIn(['meta', 'reduce_motion']);
+
+  if (reduceMotion) {
+    const { style, defaultStyle } = ownProps;
+
+    Object.keys(style).forEach(key => {
+      if (stylesToKeep.includes(key)) {
+        return;
+      }
+      // If it's setting an x or height or scale or some other value, we need
+      // to preserve the end-state value without actually animating it
+      style[key] = defaultStyle[key] = extractValue(style[key]);
+    });
+
+    return { style, defaultStyle };
+  }
+  return {};
+};
+
+export default connect(mapStateToProps)(Motion);
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index cd4cf4b32..d48e1da65 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -23,6 +23,7 @@ class UserSettingsDecorator
     user.settings['boost_modal']         = boost_modal_preference if change?('setting_boost_modal')
     user.settings['delete_modal']        = delete_modal_preference if change?('setting_delete_modal')
     user.settings['auto_play_gif']       = auto_play_gif_preference if change?('setting_auto_play_gif')
+    user.settings['reduce_motion']       = reduce_motion_preference if change?('setting_reduce_motion')
     user.settings['system_font_ui']      = system_font_ui_preference if change?('setting_system_font_ui')
     user.settings['noindex']             = noindex_preference if change?('setting_noindex')
     user.settings['theme']               = theme_preference if change?('setting_theme')
@@ -64,6 +65,10 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_auto_play_gif'
   end
 
+  def reduce_motion_preference
+    boolean_cast_setting 'setting_reduce_motion'
+  end
+
   def noindex_preference
     boolean_cast_setting 'setting_noindex'
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3bf069a31..325e27f44 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -102,6 +102,10 @@ class User < ApplicationRecord
     settings.auto_play_gif
   end
 
+  def setting_reduce_motion
+    settings.reduce_motion
+  end
+
   def setting_system_font_ui
     settings.system_font_ui
   end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index e2f15a601..a8db73161 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -25,6 +25,7 @@ class InitialStateSerializer < ActiveModel::Serializer
       store[:boost_modal]    = object.current_account.user.setting_boost_modal
       store[:delete_modal]   = object.current_account.user.setting_delete_modal
       store[:auto_play_gif]  = object.current_account.user.setting_auto_play_gif
+      store[:reduce_motion]  = object.current_account.user.setting_reduce_motion
     end
 
     store
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 7475e3fd2..69e26a7be 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -35,6 +35,7 @@
 
   .fields-group
     = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
+    = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
     = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
 
   .actions
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index fa9ccd1f0..ceb796743 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -24,7 +24,7 @@
       - video = status.media_attachments.first
       %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}
     - else
-      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
+      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
   - elsif status.preview_cards.first
     %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
 
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 86c80290c..5a58e8667 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -39,6 +39,7 @@ en:
         otp_attempt: Two-factor code
         password: Password
         setting_auto_play_gif: Auto-play animated GIFs
+        setting_reduce_motion: Reduce motion in animations
         setting_boost_modal: Show confirmation dialog before boosting
         setting_default_privacy: Post privacy
         setting_default_sensitive: Always mark media as sensitive
diff --git a/config/settings.yml b/config/settings.yml
index c437b4ccb..11681d7ec 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -22,6 +22,7 @@ defaults: &defaults
   boost_modal: false
   delete_modal: true
   auto_play_gif: false
+  reduce_motion: false
   system_font_ui: false
   noindex: false
   theme: 'default'
-- 
cgit 


From bf0ee1a25c10105e096d677782d9c0ae3e36f5a5 Mon Sep 17 00:00:00 2001
From: Yamagishi Kazutoshi <ykzts@desire.sh>
Date: Mon, 16 Oct 2017 18:12:09 +0900
Subject: Enable ESLint rules import/* (#5414)

* Enable ESLint rules import/*

* fix
---
 .eslintrc.yml                                      | 23 +++++++++-
 app/javascript/mastodon/base_polyfills.js          |  2 +-
 app/javascript/mastodon/containers/mastodon.js     |  1 +
 .../containers/autosuggest_status_container.js     | 15 ------
 .../mastodon/features/emoji/emoji_compressed.js    |  3 +-
 app/javascript/mastodon/main.js                    |  2 +-
 app/javascript/packs/common.js                     |  2 +-
 package.json                                       |  2 +
 yarn.lock                                          | 53 +++++++++++++++++++++-
 9 files changed, 81 insertions(+), 22 deletions(-)
 delete mode 100644 app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js

(limited to 'app')

diff --git a/.eslintrc.yml b/.eslintrc.yml
index 0172d7a9d..7c6da9d57 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -12,6 +12,7 @@ parser: babel-eslint
 plugins:
 - react
 - jsx-a11y
+- import
 
 parserOptions:
   sourceType: module
@@ -22,8 +23,14 @@ parserOptions:
     modules: true
     spread: true
 
-rules:
+settings:
+  import/extensions:
+  - .js
+  import/ignore:
+  - node_modules
+  - \\.(css|scss|json)$
 
+rules:
   brace-style: warn
   comma-dangle:
   - error
@@ -126,3 +133,17 @@ rules:
   jsx-a11y/role-supports-aria-props: off
   jsx-a11y/scope: warn
   jsx-a11y/tabindex-no-positive: warn
+
+  import/extensions:
+  - error
+  - always
+  - js: never
+  import/newline-after-import: error
+  import/no-extraneous-dependencies:
+  - error
+  - devDependencies:
+    - "config/webpack/**"
+    - "app/javascript/mastodon/test_setup.js"
+    - "app/javascript/**/__tests__/**"
+  import/no-unresolved: error
+  import/no-webpack-loader-syntax: error
diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js
index 266a0020c..7856b26f9 100644
--- a/app/javascript/mastodon/base_polyfills.js
+++ b/app/javascript/mastodon/base_polyfills.js
@@ -1,5 +1,5 @@
 import 'intl';
-import 'intl/locale-data/jsonp/en.js';
+import 'intl/locale-data/jsonp/en';
 import 'es6-symbol/implement';
 import includes from 'array-includes';
 import assign from 'object-assign';
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index ff27a9319..56b7bda46 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -10,6 +10,7 @@ import { hydrateStore } from '../actions/store';
 import { connectUserStream } from '../actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
+
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
diff --git a/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js b/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js
deleted file mode 100644
index a9e3a9edf..000000000
--- a/app/javascript/mastodon/features/compose/containers/autosuggest_status_container.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { connect } from 'react-redux';
-import AutosuggestStatus from '../components/autosuggest_status';
-import { makeGetStatus } from '../../../selectors';
-
-const makeMapStateToProps = () => {
-  const getStatus = makeGetStatus();
-
-  const mapStateToProps = (state, { id }) => ({
-    status: getStatus(state, id),
-  });
-
-  return mapStateToProps;
-};
-
-export default connect(makeMapStateToProps)(AutosuggestStatus);
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js
index 3bd89cf3b..c0cba952a 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.js
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js
@@ -9,7 +9,8 @@ const { unicodeToFilename } = require('./unicode_to_filename');
 const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
 const emojiMap         = require('./emoji_map.json');
 const { emojiIndex } = require('emoji-mart');
-const emojiMartData = require('emoji-mart/dist/data').default;
+const { default: emojiMartData } = require('emoji-mart/dist/data');
+
 const excluded       = ['ยฎ', 'ยฉ', 'โ„ข'];
 const skins          = ['๐Ÿป', '๐Ÿผ', '๐Ÿฝ', '๐Ÿพ', '๐Ÿฟ'];
 const shortcodeMap   = {};
diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index b418ab2f2..23b6b04fa 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -1,5 +1,5 @@
 import * as WebPushSubscription from './web_push_subscription';
-import Mastodon from 'mastodon/containers/mastodon';
+import Mastodon from './containers/mastodon';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ready from './ready';
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
index 4880f0242..96e6f4b16 100644
--- a/app/javascript/packs/common.js
+++ b/app/javascript/packs/common.js
@@ -1,6 +1,6 @@
 import { start } from 'rails-ujs';
+import 'font-awesome/css/font-awesome.css';
 
-require('font-awesome/css/font-awesome.css');
 require.context('../images/', true);
 
 start();
diff --git a/package.json b/package.json
index cf8069e94..e398730c4 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "immutable": "^3.8.1",
     "intersection-observer": "^0.4.0",
     "intl": "^1.2.5",
+    "intl-messageformat": "^2.1.0",
     "intl-relativeformat": "^2.0.0",
     "is-nan": "^1.2.1",
     "js-yaml": "^3.9.0",
@@ -121,6 +122,7 @@
     "enzyme": "^3.0.0",
     "enzyme-adapter-react-16": "^1.0.0",
     "eslint": "^3.19.0",
+    "eslint-plugin-import": "^2.7.0",
     "eslint-plugin-jsx-a11y": "^4.0.0",
     "eslint-plugin-react": "^6.10.3",
     "jest": "^21.2.1",
diff --git a/yarn.lock b/yarn.lock
index 9e25522d1..57860218c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1285,7 +1285,7 @@ buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
-builtin-modules@^1.0.0:
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
 
@@ -1623,6 +1623,10 @@ constants-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
 
+contains-path@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
 content-disposition@0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
@@ -2106,7 +2110,7 @@ dns-txt@^2.0.2:
   dependencies:
     buffer-indexof "^1.0.0"
 
-doctrine@^1.2.2:
+doctrine@1.5.0, doctrine@^1.2.2:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
   dependencies:
@@ -2396,6 +2400,35 @@ escope@^3.6.0:
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
+eslint-import-resolver-node@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
+  dependencies:
+    debug "^2.6.8"
+    resolve "^1.2.0"
+
+eslint-module-utils@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+  dependencies:
+    debug "^2.6.8"
+    pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.7.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz#21de33380b9efb55f5ef6d2e210ec0e07e7fa69f"
+  dependencies:
+    builtin-modules "^1.1.1"
+    contains-path "^0.1.0"
+    debug "^2.6.8"
+    doctrine "1.5.0"
+    eslint-import-resolver-node "^0.3.1"
+    eslint-module-utils "^2.1.1"
+    has "^1.0.1"
+    lodash.cond "^4.3.0"
+    minimatch "^3.0.3"
+    read-pkg-up "^2.0.0"
+
 eslint-plugin-jsx-a11y@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-4.0.0.tgz#779bb0fe7b08da564a422624911de10061e048ee"
@@ -4240,6 +4273,10 @@ lodash.clonedeep@^4.3.2:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
 
+lodash.cond@^4.3.0:
+  version "4.5.2"
+  resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
 lodash.defaults@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c"
@@ -5142,6 +5179,12 @@ pinkie@^2.0.0:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
 
+pkg-dir@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+  dependencies:
+    find-up "^1.0.0"
+
 pkg-dir@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
@@ -6355,6 +6398,12 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.3:
   dependencies:
     path-parse "^1.0.5"
 
+resolve@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+  dependencies:
+    path-parse "^1.0.5"
+
 restore-cursor@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
-- 
cgit 


From 1e7b3bf6257dc7fd12c5bafffb941f84bce4b982 Mon Sep 17 00:00:00 2001
From: JeanGauthier <32121978+JeanGauthier@users.noreply.github.com>
Date: Mon, 16 Oct 2017 14:09:26 +0200
Subject: i18n ultim hour ยซMoreยป dropdown title + reduce motion (#5415)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Correction fem. form

* More dropdown title

* More dropdown title

* More dropdown title

* Add option to reduce motion (#5393)
---
 app/javascript/mastodon/locales/ca.json | 1 +
 app/javascript/mastodon/locales/es.json | 1 +
 app/javascript/mastodon/locales/oc.json | 1 +
 config/locales/oc.yml                   | 4 ++--
 config/locales/simple_form.oc.yml       | 1 +
 5 files changed, 6 insertions(+), 2 deletions(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index fe2433591..1130e6c09 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -184,6 +184,7 @@
   "status.load_more": "Carrega mรฉs",
   "status.media_hidden": "Multimรจdia amagat",
   "status.mention": "Esmentar @{name}",
+  "status.more": "Mรฉs",
   "status.mute_conversation": "Silenciar conversaciรณ",
   "status.open": "Ampliar aquest estat",
   "status.pin": "Fixat en el perfil",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index f6bfbb04d..03dd9ce02 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -179,6 +179,7 @@
   "status.load_more": "Cargar mรกs",
   "status.media_hidden": "Contenido multimedia oculto",
   "status.mention": "Mencionar",
+  "status.more": "Mรกs",
   "status.mute_conversation": "Silenciar conversaciรณn",
   "status.open": "Expandir estado",
   "status.pin": "Fijar",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 4715f60ef..773f2d1e4 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -179,6 +179,7 @@
   "status.load_more": "Cargar mai",
   "status.media_hidden": "Mรจdia rescondut",
   "status.mention": "Mencionar",
+  "status.more": "Mai",
   "status.mute_conversation": "Rescondre la conversacion",
   "status.open": "Desplegar aqueste estatut",
   "status.pin": "Penjar al perfil",
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 2d72d247f..b30f797c7 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -120,9 +120,9 @@ oc:
       destroyed_msg: Nรฒta de moderacion ben suprimidaโ€ฏ!
 
     custom_emojis:
-      copied_msg: Cรฒpia locale de lโ€™emoji ben creada
+      copied_msg: Cรฒpia locala de lโ€™emoji ben creada
       copy: Copiar
-      copy_failed_msg: Fracร s de la cรฒpia locale de lโ€™emoji
+      copy_failed_msg: Fracร s de la cรฒpia locala de lโ€™emoji
       created_msg: Emoji ben creatโ€ฏ!
       delete: Suprimir
       destroyed_msg: Emojo ben suprimitโ€ฏ!
diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml
index d45f98e66..c1d5e2008 100644
--- a/config/locales/simple_form.oc.yml
+++ b/config/locales/simple_form.oc.yml
@@ -38,6 +38,7 @@ oc:
         otp_attempt: Cรฒdi Two-factor
         password: Senhal
         setting_auto_play_gif: Lectura automatica dels GIFS animats
+        setting_reduce_motion: Reduire la velocitat de las animacions
         setting_boost_modal: Afichar una fenรจstra de confirmacion abans de partejar un estatut
         setting_default_privacy: Confidencialitat de las publicacions
         setting_default_sensitive: Totjorn marcar los mรจdias coma sensibles
-- 
cgit 


From 3c530d95f66c3408b438aba924e943e4af35f92e Mon Sep 17 00:00:00 2001
From: Marcin Mikoล‚ajczak <me@m4sk.in>
Date: Mon, 16 Oct 2017 14:09:51 +0200
Subject: i18n: Update Polish translation (#5416)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Marcin Mikoล‚ajczak <me@m4sk.in>
---
 app/javascript/mastodon/locales/pl.json | 1 +
 config/locales/simple_form.en.yml       | 2 +-
 config/locales/simple_form.pl.yml       | 1 +
 3 files changed, 3 insertions(+), 1 deletion(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index c8228c0cb..77da77e10 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -179,6 +179,7 @@
   "status.load_more": "Zaล‚aduj wiฤ™cej",
   "status.media_hidden": "Zawartoล›ฤ‡ multimedialna ukryta",
   "status.mention": "Wspomnij o @{name}",
+  "status.more": "Wiฤ™cej",
   "status.mute_conversation": "Wycisz konwersacjฤ™",
   "status.open": "Rozszerz ten wpis",
   "status.pin": "Przypnij do profilu",
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 5a58e8667..aafae48ce 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -39,12 +39,12 @@ en:
         otp_attempt: Two-factor code
         password: Password
         setting_auto_play_gif: Auto-play animated GIFs
-        setting_reduce_motion: Reduce motion in animations
         setting_boost_modal: Show confirmation dialog before boosting
         setting_default_privacy: Post privacy
         setting_default_sensitive: Always mark media as sensitive
         setting_delete_modal: Show confirmation dialog before deleting a toot
         setting_noindex: Opt-out of search engine indexing
+        setting_reduce_motion: Reduce motion in animations
         setting_system_font_ui: Use system's default font
         setting_theme: Site theme
         setting_unfollow_modal: Show confirmation dialog before unfollowing someone
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index e5d408973..68f84d109 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -48,6 +48,7 @@ pl:
         setting_default_sensitive: Zawsze oznaczaj zawartoล›ฤ‡ multimedialnฤ… jako wraลผliwฤ…
         setting_delete_modal: Pytaj o potwierdzenie przed usuniฤ™ciem wpisu
         setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
+        setting_reduce_motion: Ogranicz ruch w animacjach
         setting_system_font_ui: Uลผywaj domyล›lnej czcionki systemu
         setting_theme: Motyw strony
         setting_unfollow_modal: Pytaj o potwierdzenie przed cofniฤ™ciem ล›ledzenia
-- 
cgit 


From f72936b4e696049a9829fc84b0ec5a84e4ecf7bb Mon Sep 17 00:00:00 2001
From: KY <tkbky@users.noreply.github.com>
Date: Mon, 16 Oct 2017 20:10:12 +0800
Subject: Fix #5082 - disable retweet link for followers only toot (#5397)

* Fix #5082 - disable retweet link for followers only toot

* Hide reblog count when it is a direct message
---
 .../features/status/components/detailed_status.js  | 26 +++++++++++++++++-----
 .../stream_entries/_detailed_status.html.haml      | 13 ++++++++---
 2 files changed, 30 insertions(+), 9 deletions(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 4fd1c2ec0..c10e2c531 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -43,6 +43,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 
     let media           = '';
     let applicationLink = '';
+    let reblogLink = '';
+    let reblogIcon = 'retweet';
 
     if (status.get('media_attachments').size > 0) {
       if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
@@ -80,6 +82,23 @@ export default class DetailedStatus extends ImmutablePureComponent {
       applicationLink = <span> ยท <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
     }
 
+    if (status.get('visibility') === 'direct') {
+      reblogIcon = 'envelope';
+    } else if (status.get('visibility') === 'private') {
+      reblogIcon = 'lock';
+    }
+
+    if (status.get('visibility') === 'private') {
+      reblogLink = <i className={`fa fa-${reblogIcon}`} />;
+    } else {
+      reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
+        <i className={`fa fa-${reblogIcon}`} />
+        <span className='detailed-status__reblogs'>
+          <FormattedNumber value={status.get('reblogs_count')} />
+        </span>
+      </Link>);
+    }
+
     return (
       <div className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
@@ -94,12 +113,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
         <div className='detailed-status__meta'>
           <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
             <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-          </a>{applicationLink} ยท <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
-            <i className='fa fa-retweet' />
-            <span className='detailed-status__reblogs'>
-              <FormattedNumber value={status.get('reblogs_count')} />
-            </span>
-          </Link> ยท <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
+          </a>{applicationLink} ยท {reblogLink} ยท <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
             <i className='fa fa-star' />
             <span className='detailed-status__favorites'>
               <FormattedNumber value={status.get('favourites_count')} />
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index ceb796743..3119ebf4b 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -39,9 +39,16 @@
       - else
         = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
       ยท
-    %span<
-      = fa_icon('retweet')
-      %span= status.reblogs_count
+    - if status.direct_visibility?
+      %span<
+        = fa_icon('envelope')
+    - elsif status.private_visibility?
+      %span<
+        = fa_icon('lock')
+    - else
+      %span<
+        = fa_icon('retweet')
+        %span= status.reblogs_count
     ยท
     %span<
       = fa_icon('star')
-- 
cgit 


From 03975dbde4488d2ec015fb95010084ae371b4546 Mon Sep 17 00:00:00 2001
From: voidSatisfaction <lourie@naver.com>
Date: Mon, 16 Oct 2017 21:39:28 +0900
Subject: Add up-to-date korean translation on client  (#5402)

* chore: add Korean translation for client

* fix: change unlisted and embed Korean words
---
 app/javascript/mastodon/locales/ko.json | 42 ++++++++++++++++++++-------------
 1 file changed, 25 insertions(+), 17 deletions(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index c1768cf8f..637acfab6 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -18,7 +18,7 @@
   "account.unblock_domain": "{domain} ์ˆจ๊น€ ํ•ด์ œ",
   "account.unfollow": "ํŒ”๋กœ์šฐ ํ•ด์ œ",
   "account.unmute": "๋ฎคํŠธ ํ•ด์ œ",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "์ „์ฒด ํ”„๋กœํ•„ ๋ณด๊ธฐ",
   "boost_modal.combo": "๋‹ค์Œ๋ถ€ํ„ฐ {combo}๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ด ๊ณผ์ •์„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
   "bundle_column_error.body": "Something went wrong while loading this component.",
   "bundle_column_error.retry": "Try again",
@@ -33,7 +33,7 @@
   "column.home": "ํ™ˆ",
   "column.mutes": "๋ฎคํŠธ ์ค‘์ธ ์‚ฌ์šฉ์ž",
   "column.notifications": "์•Œ๋ฆผ",
-  "column.pins": "๊ณ ์ •๋œ Toot",
+  "column.pins": "๊ณ ์ •๋œ ํˆฟ",
   "column.public": "์—ฐํ•ฉ ํƒ€์ž„๋ผ์ธ",
   "column_back_button.label": "๋Œ์•„๊ฐ€๊ธฐ",
   "column_header.hide_settings": "Hide settings",
@@ -47,7 +47,7 @@
   "compose_form.lock_disclaimer": "์ด ๊ณ„์ •์€ {locked}๋กœ ์„ค์ • ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ˆ„๊ตฌ๋‚˜ ์ด ๊ณ„์ •์„ ํŒ”๋กœ์šฐ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํŒ”๋กœ์›Œ ๊ณต๊ฐœ์˜ ํฌ์ŠคํŒ…์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
   "compose_form.lock_disclaimer.lock": "๋น„๊ณต๊ฐœ",
   "compose_form.placeholder": "์ง€๊ธˆ ๋ฌด์—‡์„ ํ•˜๊ณ  ์žˆ๋‚˜์š”?",
-  "compose_form.publish": "Toot",
+  "compose_form.publish": "ํˆฟ",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "์ด ๋ฏธ๋””์–ด๋ฅผ ๋ฏผ๊ฐํ•œ ๋ฏธ๋””์–ด๋กœ ์ทจ๊ธ‰",
   "compose_form.spoiler": "ํ…์ŠคํŠธ ์ˆจ๊ธฐ๊ธฐ",
@@ -63,8 +63,8 @@
   "confirmations.mute.message": "์ •๋ง๋กœ {name}๋ฅผ ๋ฎคํŠธํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "embed.instructions": "์•„๋ž˜์˜ ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ๋Œ€ํ™”๋ฅผ ์›ํ•˜๋Š” ๊ณณ์œผ๋กœ ํผ๊ฐ€์„ธ์š”.",
+  "embed.preview": "๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค:",
   "emoji_button.activity": "ํ™œ๋™",
   "emoji_button.custom": "Custom",
   "emoji_button.flags": "๊ตญ๊ธฐ",
@@ -82,7 +82,6 @@
   "empty_column.community": "๋กœ์ปฌ ํƒ€์ž„๋ผ์ธ์— ์•„๋ฌด ๊ฒƒ๋„ ์—†์Šต๋‹ˆ๋‹ค. ์•„๋ฌด๊ฑฐ๋‚˜ ์ ์–ด ๋ณด์„ธ์š”!",
   "empty_column.hashtag": "์ด ํ•ด์‹œํƒœ๊ทธ๋Š” ์•„์ง ์‚ฌ์šฉ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.",
   "empty_column.home": "์•„์ง ์•„๋ฌด๋„ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. {public}๋ฅผ ๋ณด๋Ÿฌ ๊ฐ€๊ฑฐ๋‚˜, ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์•„ ๋ณด์„ธ์š”.",
-  "empty_column.home.inactivity": "ํ™ˆ ํ”ผ๋“œ์— ์•„๋ฌด ๊ฒƒ๋„ ์—†์Šต๋‹ˆ๋‹ค. ํ•œ๋™์•ˆ ํ™œ๋™ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ณง ์›๋ž˜๋Œ€๋กœ ๋Œ์•„์˜ฌ ๊ฒƒ์ž…๋‹ˆ๋‹ค.",
   "empty_column.home.public_timeline": "์—ฐํ•ฉ ํƒ€์ž„๋ผ์ธ",
   "empty_column.notifications": "์•„์ง ์•Œ๋ฆผ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•ด ๋ณด์„ธ์š”!",
   "empty_column.public": "์—ฌ๊ธฐ์—” ์•„์ง ์•„๋ฌด ๊ฒƒ๋„ ์—†์Šต๋‹ˆ๋‹ค! ๊ณต๊ฐœ์ ์œผ๋กœ ๋ฌด์–ธ๊ฐ€ ํฌ์ŠคํŒ…ํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค ์œ ์ €๋ฅผ ํŒ”๋กœ์šฐ ํ•ด์„œ ๊ฐ€๋“ ์ฑ„์›Œ๋ณด์„ธ์š”!",
@@ -113,7 +112,7 @@
   "navigation_bar.info": "์ด ์ธ์Šคํ„ด์Šค์— ๋Œ€ํ•ด์„œ",
   "navigation_bar.logout": "๋กœ๊ทธ์•„์›ƒ",
   "navigation_bar.mutes": "๋ฎคํŠธ ์ค‘์ธ ์‚ฌ์šฉ์ž",
-  "navigation_bar.pins": "๊ณ ์ •๋œ Toot",
+  "navigation_bar.pins": "๊ณ ์ •๋œ ํˆฟ",
   "navigation_bar.preferences": "์‚ฌ์šฉ์ž ์„ค์ •",
   "navigation_bar.public_timeline": "์—ฐํ•ฉ ํƒ€์ž„๋ผ์ธ",
   "notification.favourite": "{name}๋‹˜์ด ์ฆ๊ฒจ์ฐพ๊ธฐ ํ–ˆ์Šต๋‹ˆ๋‹ค",
@@ -159,29 +158,34 @@
   "privacy.public.long": "๊ณต๊ฐœ ํƒ€์ž„๋ผ์ธ์— ํ‘œ์‹œ",
   "privacy.public.short": "๊ณต๊ฐœ",
   "privacy.unlisted.long": "๊ณต๊ฐœ ํƒ€์ž„๋ผ์ธ์— ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ",
-  "privacy.unlisted.short": "Unlisted",
+  "privacy.unlisted.short": "ํƒ€์ž„๋ผ์ธ์— ๋น„ํ‘œ์‹œ",
+  "relative_time.days": "{number}์ผ ์ „",
+  "relative_time.hours": "{number}์‹œ๊ฐ„ ์ „",
+  "relative_time.just_now": "๋ฐฉ๊ธˆ",
+  "relative_time.minutes": "{number}๋ถ„ ์ „",
+  "relative_time.seconds": "{number}์ดˆ ์ „",
   "reply_indicator.cancel": "์ทจ์†Œ",
   "report.placeholder": "์ฝ”๋ฉ˜ํŠธ",
   "report.submit": "์‹ ๊ณ ํ•˜๊ธฐ",
   "report.target": "๋ฌธ์ œ๊ฐ€ ๋œ ์‚ฌ์šฉ์ž",
   "search.placeholder": "๊ฒ€์ƒ‰",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.search_format": "๊ณ ๊ธ‰ ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•",
+  "search_popout.tips.hashtag": "ํ•ด์‹œํƒœ๊ทธ",
+  "search_popout.tips.status": "ํˆฟ",
+  "search_popout.tips.text": "๋‹จ์ˆœํ•œ ํ…์ŠคํŠธ ๊ฒ€์ƒ‰์€ ๊ด€๊ณ„๋œ ํ”„๋กœํ•„ ์ด๋ฆ„, ์œ ์ € ์ด๋ฆ„ ๊ทธ๋ฆฌ๊ณ  ํ•ด์‹œํƒœ๊ทธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค",
+  "search_popout.tips.user": "์œ ์ €",
   "search_results.total": "{count, number}๊ฑด์˜ ๊ฒฐ๊ณผ",
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "์ด ํฌ์ŠคํŠธ๋Š” ๋ถ€์ŠคํŠธ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
   "status.delete": "์‚ญ์ œ",
-  "status.embed": "Embed",
+  "status.embed": "๊ณต์œ ํ•˜๊ธฐ",
   "status.favourite": "์ฆ๊ฒจ์ฐพ๊ธฐ",
   "status.load_more": "๋” ๋ณด๊ธฐ",
   "status.media_hidden": "๋ฏธ๋””์–ด ์ˆจ๊ฒจ์ง",
   "status.mention": "๋‹ต์žฅ",
   "status.mute_conversation": "์ด ๋Œ€ํ™”๋ฅผ ๋ฎคํŠธ",
   "status.open": "์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ",
-  "status.pin": "Pin on profile",
+  "status.pin": "๊ณ ์ •",
   "status.reblog": "๋ถ€์ŠคํŠธ",
   "status.reblogged_by": "{name}๋‹˜์ด ๋ถ€์ŠคํŠธ ํ–ˆ์Šต๋‹ˆ๋‹ค",
   "status.reply": "๋‹ต์žฅ",
@@ -193,7 +197,7 @@
   "status.show_less": "์ˆจ๊ธฐ๊ธฐ",
   "status.show_more": "๋” ๋ณด๊ธฐ",
   "status.unmute_conversation": "์ด ๋Œ€ํ™”์˜ ๋ฎคํŠธ ํ•ด์ œํ•˜๊ธฐ",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "๊ณ ์ • ํ•ด์ œ",
   "tabs_bar.compose": "ํฌ์ŠคํŠธ",
   "tabs_bar.federated_timeline": "์—ฐํ•ฉ",
   "tabs_bar.home": "ํ™ˆ",
@@ -212,5 +216,9 @@
   "video.mute": "Mute sound",
   "video.pause": "Pause",
   "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.unmute": "Unmute sound",
+  "video_player.expand": "Expand video",
+  "video_player.toggle_sound": "Toggle sound",
+  "video_player.toggle_visible": "Toggle visibility",
+  "video_player.video_error": "Video could not be played"
 }
-- 
cgit 


From 6f490b4bfed5fba9bd543a4c99b5694f37cd1f99 Mon Sep 17 00:00:00 2001
From: unarist <m.unarist@gmail.com>
Date: Mon, 16 Oct 2017 22:58:23 +0900
Subject: Fix un-reblogged status being at wrong position in the home timeline
 (#5418)

We've changed un-reblogging behavior when we implement Snowflake, to insert un-reblogged status at the position reblogging status existed.

However, our API expects home timeline is ordered by status ids, and max_id/since_id filters by zset score. Due to this, un-reblogged status appears as a last item of result set, and timeline expansion may skips many statuses.

So this reverts that change...reblogged status inserted at corresponding position to its id.
---
 app/lib/feed_manager.rb       | 9 +++++----
 spec/lib/feed_manager_spec.rb | 9 ++++++---
 2 files changed, 11 insertions(+), 7 deletions(-)

(limited to 'app')

diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 89aeaadcd..dfd11a23b 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -198,10 +198,11 @@ class FeedManager
       # 2. Remove the reblogged status from the `:reblogs` zset.
       redis.zrem(reblog_key, status.reblog_of_id)
 
-      # 3. Add the reblogged status to the feed using the reblogging
-      # status' ID as its score, and the reblogged status' ID as its
-      # value.
-      redis.zadd(timeline_key, status.id, status.reblog_of_id)
+      # 3. Add the reblogged status to the feed.
+      # Note that we can't use old score in here
+      # and it must be an ID of corresponding status
+      # because we need to filter timeline by status ID.
+      redis.zadd(timeline_key, status.reblog_of_id, status.reblog_of_id)
 
       # 4. Remove the reblogging status from the feed (as normal)
     end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 923894ccb..643f1f003 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -233,19 +233,22 @@ RSpec.describe FeedManager do
   describe '#unpush' do
     it 'leaves a reblogged status when deleting the reblog' do
       account = Fabricate(:account)
-      reblogged = Fabricate(:status)
+      reblogged = Fabricate(:status, id: Mastodon::Snowflake.id_at(2.day.ago.utc))
+      other_status = Fabricate(:status, id: Mastodon::Snowflake.id_at(1.day.ago.utc))
       status = Fabricate(:status, reblog: reblogged)
 
+      FeedManager.instance.push('type', account, other_status)
       FeedManager.instance.push('type', account, status)
 
       # The reblogging status should show up under normal conditions.
-      expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [status.id.to_s]
+      expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [other_status.id.to_s, status.id.to_s]
 
       FeedManager.instance.unpush('type', account, status)
 
       # Because we couldn't tell if the status showed up any other way,
       # we had to stick the reblogged status in by itself.
-      expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [reblogged.id.to_s]
+      # And it must be ordered by status ids.
+      expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [reblogged.id.to_s, other_status.id.to_s]
     end
 
     it 'sends push updates' do
-- 
cgit 


From aec70b44fc551db6471c8bc5210688b154ac661f Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 16 Oct 2017 15:59:30 +0200
Subject: Filter out duplicate IDs in timelines reducer (#5417)

Possibly the cause of #5379, #5377
---
 app/javascript/mastodon/reducers/timelines.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'app')

diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index b17d74ef3..c3f117647 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -31,10 +31,10 @@ const initialTimeline = ImmutableMap({
 });
 
 const normalizeTimeline = (state, timeline, statuses, next) => {
-  const ids       = ImmutableList(statuses.map(status => status.get('id')));
+  const oldIds    = state.getIn([timeline, 'items'], ImmutableList());
+  const ids       = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId));
   const wasLoaded = state.getIn([timeline, 'loaded']);
   const hadNext   = state.getIn([timeline, 'next']);
-  const oldIds    = state.getIn([timeline, 'items'], ImmutableList());
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('loaded', true);
@@ -45,8 +45,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => {
 };
 
 const appendNormalizedTimeline = (state, timeline, statuses, next) => {
-  const ids    = ImmutableList(statuses.map(status => status.get('id')));
   const oldIds = state.getIn([timeline, 'items'], ImmutableList());
+  const ids    = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId));
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('isLoading', false);
-- 
cgit