about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--app/assets/images/mastodon.jpgbin0 -> 154354 bytes
-rw-r--r--app/assets/images/mastodon_small.jpgbin0 -> 131474 bytes
-rw-r--r--app/assets/javascripts/application_public.js1
-rw-r--r--app/assets/javascripts/components/actions/compose.jsx3
-rw-r--r--app/assets/javascripts/components/components/autosuggest_textarea.jsx155
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx123
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx6
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx8
-rw-r--r--app/assets/javascripts/extras.jsx20
-rw-r--r--app/assets/stylesheets/application.scss12
-rw-r--r--app/assets/stylesheets/components.scss40
-rw-r--r--app/assets/stylesheets/stream_entries.scss378
-rw-r--r--app/controllers/api/oembed_controller.rb4
-rw-r--r--app/controllers/stream_entries_controller.rb12
-rw-r--r--app/helpers/stream_entries_helper.rb4
-rw-r--r--app/models/media_attachment.rb4
-rw-r--r--app/models/tag.rb2
-rw-r--r--app/services/favourite_service.rb1
-rw-r--r--app/services/follow_service.rb1
-rw-r--r--app/services/post_status_service.rb1
-rw-r--r--app/services/reblog_service.rb1
-rw-r--r--app/services/remove_status_service.rb1
-rw-r--r--app/views/about/index.html.haml9
-rw-r--r--app/views/accounts/show.atom.ruby1
-rw-r--r--app/views/api/oembed/show.json.rabl4
-rw-r--r--app/views/home/index.html.haml1
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/layouts/embedded.html.haml8
-rw-r--r--app/views/stream_entries/_content_spoiler.html.haml3
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml36
-rw-r--r--app/views/stream_entries/_simple_status.html.haml28
-rw-r--r--app/views/stream_entries/_status.html.haml25
-rw-r--r--app/views/stream_entries/embed.html.haml2
-rw-r--r--app/workers/hub_ping_worker.rb12
-rw-r--r--config/i18n-tasks.yml1
-rw-r--r--config/initializers/ostatus.rb1
-rw-r--r--config/locales/en.yml19
-rw-r--r--config/routes.rb6
-rw-r--r--package.json11
-rw-r--r--public/500.html1
-rw-r--r--spec/controllers/api/v1/follows_controller_spec.rb5
-rw-r--r--spec/services/reblog_service_spec.rb5
-rw-r--r--storybook/config.js12
-rw-r--r--storybook/stories/autosuggest_textarea.story.jsx6
-rw-r--r--storybook/stories/button.story.jsx1
-rw-r--r--storybook/stories/loading_indicator.story.jsx6
-rw-r--r--storybook/stories/tabs_bar.story.jsx6
-rw-r--r--storybook/storybook.css3
-rw-r--r--storybook/storybook.scss15
-rw-r--r--storybook/webpack.config.js13
-rw-r--r--yarn.lock524
52 files changed, 1183 insertions, 360 deletions
diff --git a/README.md b/README.md
index 0eb9bbe3e..3add10473 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,6 @@ If you would like, you can [support the development of this project on Patreon][
 
 - `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
 - `LOCAL_HTTPS` set it to `true` if HTTPS works on your website. This is used to generate canonical URLs, which is also important when generating and parsing federation-related IDs
-- `HUB_URL` should be the URL of the PubsubHubbub service that your instance is going to use. By default it is the open service of Superfeedr
 
 Consult the example configuration file, `.env.production.sample` for the full list. Among other things you need to set details for the SMTP server you are going to use.
 
diff --git a/app/assets/images/mastodon.jpg b/app/assets/images/mastodon.jpg
new file mode 100644
index 000000000..f22a252a6
--- /dev/null
+++ b/app/assets/images/mastodon.jpg
Binary files differdiff --git a/app/assets/images/mastodon_small.jpg b/app/assets/images/mastodon_small.jpg
new file mode 100644
index 000000000..cb8cdc992
--- /dev/null
+++ b/app/assets/images/mastodon_small.jpg
Binary files differdiff --git a/app/assets/javascripts/application_public.js b/app/assets/javascripts/application_public.js
index 31a96fd2d..f131a267a 100644
--- a/app/assets/javascripts/application_public.js
+++ b/app/assets/javascripts/application_public.js
@@ -1,2 +1,3 @@
 //= require jquery
 //= require jquery_ujs
+//= require extras
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index ec5465381..a9fbe6b91 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -185,13 +185,14 @@ export function readyComposeSuggestions(token, accounts) {
   };
 };
 
-export function selectComposeSuggestion(position, accountId) {
+export function selectComposeSuggestion(position, token, accountId) {
   return (dispatch, getState) => {
     const completion = getState().getIn(['accounts', accountId, 'acct']);
 
     dispatch({
       type: COMPOSE_SUGGESTION_SELECT,
       position,
+      token,
       completion
     });
   };
diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
new file mode 100644
index 000000000..95ca5f2f6
--- /dev/null
+++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
@@ -0,0 +1,155 @@
+import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+const textAtCursorMatchesToken = (str, caretPosition) => {
+  let word;
+
+  let left  = str.slice(0, caretPosition).search(/\S+$/);
+  let right = str.slice(caretPosition).search(/\s/);
+
+  if (right < 0) {
+    word = str.slice(left);
+  } else {
+    word = str.slice(left, right + caretPosition);
+  }
+
+  if (!word || word.trim().length < 2 || word[0] !== '@') {
+    return [null, null];
+  }
+
+  word = word.trim().toLowerCase().slice(1);
+
+  if (word.length > 0) {
+    return [left + 1, word];
+  } else {
+    return [null, null];
+  }
+};
+
+const AutosuggestTextarea = React.createClass({
+
+  propTypes: {
+    value: React.PropTypes.string,
+    suggestions: ImmutablePropTypes.list,
+    disabled: React.PropTypes.bool,
+    placeholder: React.PropTypes.string,
+    onSuggestionSelected: React.PropTypes.func.isRequired,
+    onSuggestionsClearRequested: React.PropTypes.func.isRequired,
+    onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
+    onChange: React.PropTypes.func.isRequired,
+    onKeyUp: React.PropTypes.func
+  },
+
+  getInitialState () {
+    return {
+      suggestionsHidden: false,
+      selectedSuggestion: 0,
+      lastToken: null,
+      tokenStart: 0
+    };
+  },
+
+  onChange (e) {
+    const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
+
+    if (token != null && this.state.lastToken !== token) {
+      this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
+      this.props.onSuggestionsFetchRequested(token);
+    } else if (token === null && this.state.lastToken != null) {
+      this.setState({ lastToken: null });
+      this.props.onSuggestionsClearRequested();
+    }
+
+    this.props.onChange(e);
+  },
+
+  onKeyDown (e) {
+    const { suggestions, disabled } = this.props;
+    const { selectedSuggestion, suggestionsHidden } = this.state;
+
+    if (disabled) {
+      e.preventDefault();
+      return;
+    }
+
+    switch(e.key) {
+      case 'Escape':
+        if (!suggestionsHidden) {
+          e.preventDefault();
+          this.setState({ suggestionsHidden: true });
+        }
+
+        break;
+      case 'ArrowDown':
+        if (suggestions.size > 0 && !suggestionsHidden) {
+          e.preventDefault();
+          this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
+        }
+
+        break;
+      case 'ArrowUp':
+        if (suggestions.size > 0 && !suggestionsHidden) {
+          e.preventDefault();
+          this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
+        }
+
+        break;
+      case 'Enter':
+      case 'Tab':
+        // Select suggestion
+        if (this.state.lastToken != null && suggestions.size > 0 && !suggestionsHidden) {
+          e.preventDefault();
+          e.stopPropagation();
+          this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
+        }
+
+        break;
+    }
+  },
+
+  onSuggestionClick (suggestion, e) {
+    e.preventDefault();
+    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
+  },
+
+  componentWillReceiveProps (nextProps) {
+    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
+      this.setState({ suggestionsHidden: false });
+    }
+  },
+
+  setTextarea (c) {
+    this.textarea = c;
+  },
+
+  render () {
+    const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
+    const { suggestionsHidden, selectedSuggestion } = this.state;
+
+    return (
+      <div className='autosuggest-textarea'>
+        <textarea
+          ref={this.setTextarea}
+          className='autosuggest-textarea__textarea'
+          disabled={disabled}
+          placeholder={placeholder}
+          value={value}
+          onChange={this.onChange}
+          onKeyDown={this.onKeyDown}
+          onKeyUp={onKeyUp}
+        />
+
+        <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
+          {suggestions.map((suggestion, i) => (
+            <div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
+              <AutosuggestAccountContainer id={suggestion} />
+            </div>
+          ))}
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default AutosuggestTextarea;
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index 00589b3c8..200502dad 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -4,7 +4,7 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ReplyIndicator from './reply_indicator';
 import UploadButton from './upload_button';
-import Autosuggest from 'react-autosuggest';
+import AutosuggestTextarea from '../../../components/autosuggest_textarea';
 import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container';
 import { debounce } from 'react-decoration';
 import UploadButtonContainer from '../containers/upload_button_container';
@@ -16,59 +16,12 @@ const messages = defineMessages({
   publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
 });
 
-const getTokenForSuggestions = (str, caretPosition) => {
-  let word;
-
-  let left  = str.slice(0, caretPosition).search(/\S+$/);
-  let right = str.slice(caretPosition).search(/\s/);
-
-  if (right < 0) {
-    word = str.slice(left);
-  } else {
-    word = str.slice(left, right + caretPosition);
-  }
-
-  if (!word || word.trim().length < 2 || word[0] !== '@') {
-    return null;
-  }
-
-  word = word.trim().toLowerCase().slice(1);
-
-  if (word.length > 0) {
-    return word;
-  } else {
-    return null;
-  }
-};
-
-const getSuggestionValue = suggestionId => suggestionId;
-const renderSuggestion   = suggestionId => <AutosuggestAccountContainer id={suggestionId} />;
-
-const textareaStyle = {
-  display: 'block',
-  boxSizing: 'border-box',
-  width: '100%',
-  height: '100px',
-  resize: 'none',
-  border: 'none',
-  color: '#282c37',
-  padding: '10px',
-  fontFamily: 'Roboto',
-  fontSize: '14px',
-  margin: '0',
-  resize: 'vertical'
-};
-
-const renderInputComponent = inputProps => (
-  <textarea {...inputProps} className='compose-form__textarea' style={textareaStyle} />
-);
-
 const ComposeForm = React.createClass({
 
   propTypes: {
     text: React.PropTypes.string.isRequired,
     suggestion_token: React.PropTypes.string,
-    suggestions: React.PropTypes.array,
+    suggestions: ImmutablePropTypes.list,
     sensitive: React.PropTypes.bool,
     unlisted: React.PropTypes.bool,
     is_submitting: React.PropTypes.bool,
@@ -87,10 +40,6 @@ const ComposeForm = React.createClass({
   mixins: [PureRenderMixin],
 
   handleChange (e) {
-    if (typeof e.target.value === 'undefined' || typeof e.target.value === 'number') {
-      return;
-    }
-
     this.props.onChange(e.target.value);
   },
 
@@ -104,45 +53,17 @@ const ComposeForm = React.createClass({
     this.props.onSubmit();
   },
 
-  componentDidUpdate (prevProps) {
-    if (prevProps.text !== this.props.text || prevProps.in_reply_to !== this.props.in_reply_to) {
-      const textarea = this.autosuggest.input;
-
-      if (textarea) {
-        textarea.focus();
-      }
-    }
-  },
-
   onSuggestionsClearRequested () {
     this.props.onClearSuggestions();
   },
 
   @debounce(500)
-  onSuggestionsFetchRequested ({ value }) {
-    const textarea = this.autosuggest.input;
-
-    if (textarea) {
-      const token = getTokenForSuggestions(value, textarea.selectionStart);
-
-      if (token !== null) {
-        this.props.onFetchSuggestions(token);
-      } else {
-        this.props.onClearSuggestions();
-      }
-    }
-  },
-
-  onSuggestionSelected (e, { suggestionValue }) {
-    const textarea = this.autosuggest.input;
-
-    if (textarea) {
-      this.props.onSuggestionSelected(textarea.selectionStart, suggestionValue);
-    }
+  onSuggestionsFetchRequested (token) {
+    this.props.onFetchSuggestions(token);
   },
 
-  setRef (c) {
-    this.autosuggest = c;
+  onSuggestionSelected (tokenStart, token, value) {
+    this.props.onSuggestionSelected(tokenStart, token, value);
   },
 
   handleChangeSensitivity (e) {
@@ -153,6 +74,16 @@ const ComposeForm = React.createClass({
     this.props.onChangeVisibility(e.target.checked);
   },
 
+  componentDidUpdate (prevProps) {
+    if (prevProps.in_reply_to !== this.props.in_reply_to) {
+      this.autosuggestTextarea.textarea.focus();
+    }
+  },
+
+  setAutosuggestTextarea (c) {
+    this.autosuggestTextarea = c;
+  },
+
   render () {
     const { intl } = this.props;
     let replyArea  = '';
@@ -162,29 +93,21 @@ const ComposeForm = React.createClass({
       replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
     }
 
-    const inputProps = {
-      placeholder: intl.formatMessage(messages.placeholder),
-      value: this.props.text,
-      onKeyUp: this.handleKeyUp,
-      onChange: this.handleChange,
-      disabled: disabled
-    };
-
     return (
       <div style={{ padding: '10px' }}>
         {replyArea}
 
-        <Autosuggest
-          ref={this.setRef}
+        <AutosuggestTextarea
+          ref={this.setAutosuggestTextarea}
+          placeholder={intl.formatMessage(messages.placeholder)}
+          disabled={disabled}
+          value={this.props.text}
+          onChange={this.handleChange}
           suggestions={this.props.suggestions}
-          focusFirstSuggestion={true}
+          onKeyUp={this.handleKeyUp}
           onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
           onSuggestionsClearRequested={this.onSuggestionsClearRequested}
           onSuggestionSelected={this.onSuggestionSelected}
-          getSuggestionValue={getSuggestionValue}
-          renderSuggestion={renderSuggestion}
-          renderInputComponent={renderInputComponent}
-          inputProps={inputProps}
         />
 
         <div style={{ marginTop: '10px', overflow: 'hidden' }}>
diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
index 8aa719476..c774b2687 100644
--- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
@@ -19,7 +19,7 @@ const makeMapStateToProps = () => {
     return {
       text: state.getIn(['compose', 'text']),
       suggestion_token: state.getIn(['compose', 'suggestion_token']),
-      suggestions: state.getIn(['compose', 'suggestions']).toJS(),
+      suggestions: state.getIn(['compose', 'suggestions']),
       sensitive: state.getIn(['compose', 'sensitive']),
       unlisted: state.getIn(['compose', 'unlisted']),
       is_submitting: state.getIn(['compose', 'is_submitting']),
@@ -53,8 +53,8 @@ const mapDispatchToProps = function (dispatch) {
       dispatch(fetchComposeSuggestions(token));
     },
 
-    onSuggestionSelected (position, accountId) {
-      dispatch(selectComposeSuggestion(position, accountId));
+    onSuggestionSelected (position, token, accountId) {
+      dispatch(selectComposeSuggestion(position, token, accountId));
     },
 
     onChangeSensitivity (checked) {
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index 9d1d53083..4bb76dff0 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -75,11 +75,9 @@ function removeMedia(state, mediaId) {
   });
 };
 
-const insertSuggestion = (state, position, completion) => {
-  const token = state.get('suggestion_token');
-
+const insertSuggestion = (state, position, token, completion) => {
   return state.withMutations(map => {
-    map.update('text', oldText => `${oldText.slice(0, position - token.length)}${completion}${oldText.slice(position + token.length)}`);
+    map.update('text', oldText => `${oldText.slice(0, position)}${completion}${oldText.slice(position + token.length)}`);
     map.set('suggestion_token', null);
     map.update('suggestions', Immutable.List(), list => list.clear());
   });
@@ -130,7 +128,7 @@ export default function compose(state = initialState, action) {
     case COMPOSE_SUGGESTIONS_READY:
       return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
     case COMPOSE_SUGGESTION_SELECT:
-      return insertSuggestion(state, action.position, action.completion);
+      return insertSuggestion(state, action.position, action.token, action.completion);
     case TIMELINE_DELETE:
       if (action.id === state.get('in_reply_to')) {
         return state.set('in_reply_to', null);
diff --git a/app/assets/javascripts/extras.jsx b/app/assets/javascripts/extras.jsx
new file mode 100644
index 000000000..9fd769c0b
--- /dev/null
+++ b/app/assets/javascripts/extras.jsx
@@ -0,0 +1,20 @@
+import emojify from './components/emoji'
+
+$(() => {
+  $.each($('.entry .content, .entry .status__content, .display-name, .name, .account__header__content'), (_, content) => {
+    const $content = $(content);
+    $content.html(emojify($content.html()));
+  });
+
+  $('.video-player video').on('click', e => {
+    if (e.target.paused) {
+      e.target.play();
+    } else {
+      e.target.pause();
+    }
+  });
+
+  $('.media-spoiler').on('click', e => {
+    $(e.target).hide();
+  });
+});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 609b30726..6dd89c0ea 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -114,6 +114,18 @@ body {
     padding: 0;
   }
 
+  &.embed {
+    background: transparent;
+    margin: 0;
+
+    .container {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+    }
+  }
+
   @media screen and (max-width: 360px) {
     padding-bottom: 0;
   }
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 517fcd3f1..210e722cc 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -530,3 +530,43 @@
     background: lighten(#373b4a, 5%);
   }
 }
+
+.autosuggest-textarea {
+  position: relative;
+}
+
+.autosuggest-textarea__textarea {
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  height: 100px;
+  resize: none;
+  border: none;
+  color: #282c37;
+  padding: 10px;
+  font-family: 'Roboto';
+  font-size: 14px;
+  margin: 0;
+  resize: vertical;
+}
+
+.autosuggest-textarea__suggestions {
+  position: absolute;
+  top: 100%;
+  width: 100%;
+  z-index: 99;
+  box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
+  background: #d9e1e8;
+  color: #282c37;
+  font-size: 14px;
+}
+
+.autosuggest-textarea__suggestions__item {
+  padding: 10px;
+  cursor: pointer;
+
+  &.selected {
+    background: #2b90d9;
+    color: #fff;
+  }
+}
diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss
index 1e29ee718..5cd140aac 100644
--- a/app/assets/stylesheets/stream_entries.scss
+++ b/app/assets/stylesheets/stream_entries.scss
@@ -3,232 +3,302 @@
   box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
 
   .entry {
-    border-bottom: 1px solid #d9e1e8;
-    background: #fff;
-    border-left: 2px solid #fff;
+    .status.light, .detailed-status.light {
+      border-bottom: 1px solid #d9e1e8;
+    }
 
-    &.entry-reblog {
-      border-left-color: #2b90d9;
+    &:last-child {
+      .status.light, .detailed-status.light {
+        border-bottom: 0;
+        border-radius: 0 0 4px 4px;
+      }
     }
 
-    &.entry-predecessor, &.entry-successor {
-      background: #d9e1e8;
-      border-left-color: #d9e1e8;
-      border-bottom-color: darken(#d9e1e8, 10%);
+    &:first-child {
+      .status.light, .detailed-status.light {
+        border-radius: 4px 4px 0 0;
+      }
 
-      .header {
-        .header__right {
-          .counter-btn {
-            color: darken(#d9e1e8, 15%);
-          }
+      &:last-child {
+        .status.light, .detailed-status.light {
+          border-radius: 4px;
         }
       }
     }
+  }
 
-    &.entry-center {
-      border-bottom-color: darken(#d9e1e8, 10%);
-    }
+  .status.light {
+    padding: 14px 14px 14px (48px + 14px*2);
+    position: relative;
+    min-height: 48px;
+    cursor: default;
+    background: lighten(#d9e1e8, 8%);
 
-    &.entry-follow, &.entry-favourite {
-      .content {
-        padding-top: 10px;
-        padding-bottom: 10px;
+    .status__header {
+      font-size: 15px;
 
-        strong {
-          font-weight: 500;
+      .status__meta {
+        float: right;
+        font-size: 14px;
+
+        .status__relative-time {
+          color: #9baec8;
         }
       }
     }
 
-    &:last-child {
-      border-bottom: 0;
-      border-radius: 0 0 4px 4px;
+    .status__display-name {
+      display: block;
+      max-width: 100%;
+      padding-right: 25px;
+      color: #282c37;
     }
-  }
 
-  .entry:first-child {
-    border-radius: 4px 4px 0 0;
+    .status__avatar {
+      position: absolute;
+      left: 14px;
+      top: 14px;
+      width: 48px;
+      height: 48px;
 
-    &:last-child {
-      border-radius: 4px;
+      & > div {
+        width: 48px;
+        height: 48px;
+      }
+
+      img {
+        display: block;
+        border-radius: 4px;
+      }
     }
-  }
 
-  @media screen and (max-width: 700px) {
-    border-radius: 0;
-    box-shadow: none;
+    .display-name {
+      display: block;
+      max-width: 100%;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
 
-    .entry {
-      &:last-child {
-        border-radius: 0;
+      strong {
+        font-weight: 500;
+        color: #282c37;
       }
 
-      &:first-child {
-        border-radius: 0;
+      span {
+        font-size: 14px;
+        color: #9baec8;
+      }
+    }
 
-        &:last-child {
-          border-radius: 0;
-        }
+    .status__content {
+      color: #282c37;
+
+      a {
+        color: #2b90d9;
       }
     }
-  }
 
-  .entry__container {
-    overflow: hidden;
+    .status__attachments {
+      margin-top: 8px;
+      overflow: hidden;
+      width: 100%;
+      box-sizing: border-box;
+      height: 110px;
+      display: flex;
+    }
   }
 
-  .avatar {
-    width: 56px;
-    padding: 15px 10px;
-    padding-right: 5px;
-    float: left;
+  .detailed-status.light {
+    padding: 14px;
+    background: #fff;
+    cursor: default;
 
-    img {
-      width: 56px;
-      height: 56px;
+    .detailed-status__display-name {
       display: block;
-      border-radius: 4px;
-    }
-  }
+      overflow: hidden;
+      margin-bottom: 15px;
 
-  .entry__container__container {
-    margin-left: 71px;
-  }
+      & > div {
+        float: left;
+        margin-right: 10px;
+      }
 
-  .header {
-    margin-bottom: 10px;
-    padding: 15px;
-    padding-bottom: 0;
-    padding-left: 8px;
-    display: flex;
+      .display-name {
+        display: block;
+        max-width: 100%;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+
+        strong {
+          font-weight: 500;
+          color: #282c37;
+        }
 
-    .header__left {
-      flex: 1;
+        span {
+          font-size: 14px;
+          color: #9baec8;
+        }
+      }
     }
 
-    .header__right {
+    .avatar {
+      width: 48px;
+      height: 48px;
 
+      img {
+        display: block;
+        border-radius: 4px;
+      }
     }
 
-    .name {
-      text-decoration: none;
+    .status__content {
+      color: #282c37;
+
+      a {
+        color: #2b90d9;
+      }
+    }
+
+    .detailed-status__meta {
+      margin-top: 15px;
       color: #9baec8;
+      font-size: 14px;
+      line-height: 18px;
 
-      strong {
-        color: #282c37;
-        font-weight: 500;
+      a {
+        color: inherit;
       }
 
-      &:hover {
-        strong {
-          text-decoration: underline;
-        }
+      span > span {
+        font-weight: 500;
+        font-size: 12px;
+        margin-left: 6px;
+        display: inline-block;
       }
     }
-  }
-
-  .pre-header {
-    border-bottom: 1px solid #d9e1e8;
-    color: #2b90d9;
-    padding: 5px 10px;
-    padding-left: 8px;
-    clear: both;
 
-    .name {
-      color: #2b90d9;
-      font-weight: 500;
-      text-decoration: none;
+    .detailed-status__attachments {
+      margin-top: 8px;
+      overflow: hidden;
+      width: 100%;
+      box-sizing: border-box;
+      height: 300px;
+      display: flex;
+    }
 
-      &:hover {
-        text-decoration: underline;
+    .video-player {
+      margin-top: 8px;
+      height: 300px;
+      overflow: hidden;
+
+      video {
+        position: relative;
+        z-index: 1;
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        top: 50%;
+        transform: translateY(-50%);
       }
     }
   }
 
-  .content {
-    font-size: 14px;
-    padding: 0 15px;
-    padding-left: 8px;
-    padding-bottom: 15px;
-    color: #282c37;
-    word-wrap: break-word;
-    overflow: hidden;
-    white-space: pre-wrap;
-
-    p {
-      margin-bottom: 18px;
+  .media-item, .video-item {
+    box-sizing: border-box;
+    position: relative;
+    left: auto;
+    top: auto;
+    right: auto;
+    bottom: auto;
+    float: left;
+    border: medium none;
+    display: block;
+    flex: 1 1 auto;
+    height: 100%;
+    margin-right: 2px;
 
-      &:last-child {
-        margin-bottom: 0;
-      }
+    &:last-child {
+      margin-right: 0;
     }
 
     a {
-      color: #2b90d9;
+      display: block;
+      width: 100%;
+      height: 100%;
+      background: no-repeat scroll center center / cover;
       text-decoration: none;
+      cursor: zoom-in;
+    }
+  }
 
-      &:hover {
-        text-decoration: underline;
-      }
+  .video-item {
+    max-width: 196px;
 
-      &.mention {
-        &:hover {
-          text-decoration: none;
+    a {
+      cursor: pointer;
+    }
 
-          span {
-            text-decoration: underline;
-          }
-        }
-      }
+    .video-item__play {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      font-size: 36px;
+      transform: translate(-50%, -50%);
+      padding: 5px;
+      border-radius: 100px;
+      color: rgba(255, 255, 255, 0.8);
     }
   }
 
-  .time {
-    text-decoration: none;
-    color: #9baec8;
+  .media-spoiler {
+    background: #9baec8;
+    width: 100%;
+    height: 100%;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    text-align: center;
+    transition: all 100ms linear;
 
     &:hover {
-      text-decoration: underline;
+      background: darken(#9baec8, 5%);
     }
-  }
 
-  .media-attachments {
-    list-style: none;
-    margin: 0;
-    padding: 0;
-    display: block;
-    overflow: hidden;
-    padding-left: 10px;
-    margin-bottom: 15px;
-
-    li {
+    span {
       display: block;
-      float: left;
-      width: 120px;
-      height: 100px;
-      border-radius: 4px;
-      margin-right: 4px;
-      margin-bottom: 4px;
 
-      a {
-        display: block;
-        width: 120px;
-        height: 100px;
-        border-radius: 4px;
-        background-position: center;
-        background-repeat: none;
-        background-size: cover;
+      &:first-child {
+        font-size: 14px;
+      }
+
+      &:last-child {
+        font-size: 11px;
+        font-weight: 500;
       }
     }
   }
+}
 
-  @media screen and (max-width: 360px) {
-    .avatar {
-      display: none;
-    }
+.embed {
+  .activity-stream {
+    border-radius: 4px;
+    box-shadow: none;
 
-    .entry__container__container {
-      margin-left: 7px;
+    .entry {
+      &:last-child {
+        border-radius: 0 0 4px 4px;
+      }
+
+      &:first-child {
+        border-radius: 4px 4px 0 0;
+
+        &:last-child {
+          border-radius: 4px;
+        }
+      }
     }
   }
 }
diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb
index 4a591dc22..2360061ff 100644
--- a/app/controllers/api/oembed_controller.rb
+++ b/app/controllers/api/oembed_controller.rb
@@ -5,8 +5,8 @@ class Api::OembedController < ApiController
 
   def show
     @stream_entry = stream_entry_from_url(params[:url])
-    @width        = [300, params[:maxwidth].to_i].min
-    @height       = [200, params[:maxheight].to_i].min
+    @width        = params[:maxwidth].present?  ? params[:maxwidth].to_i  : 400
+    @height       = params[:maxheight].present? ? params[:maxheight].to_i : 600
   end
 
   private
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index 98d029030..58dd423f7 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -9,8 +9,6 @@ class StreamEntriesController < ApplicationController
   before_action :check_account_suspension
 
   def show
-    @type = @stream_entry.activity_type.downcase
-
     respond_to do |format|
       format.html do
         return gone if @stream_entry.activity.nil?
@@ -25,6 +23,15 @@ class StreamEntriesController < ApplicationController
     end
   end
 
+  def embed
+    response.headers['X-Frame-Options'] = 'ALLOWALL'
+    @external_links = true
+
+    return gone if @stream_entry.activity.nil?
+
+    render layout: 'embedded'
+  end
+
   private
 
   def set_account
@@ -37,6 +44,7 @@ class StreamEntriesController < ApplicationController
 
   def set_stream_entry
     @stream_entry = @account.stream_entries.find(params[:id])
+    @type         = @stream_entry.activity_type.downcase
   end
 
   def check_account_suspension
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 0aa7008be..5cd65008e 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -5,6 +5,10 @@ module StreamEntriesHelper
     account.display_name.blank? ? account.username : account.display_name
   end
 
+  def acct(account)
+    "@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}"
+  end
+
   def avatar_for_status_url(status)
     status.reblog? ? status.reblog.account.avatar.url( :original) : status.account.avatar.url( :original)
   end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index a97fe89a5..2a5d23739 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -45,14 +45,14 @@ class MediaAttachment < ApplicationRecord
       if f.instance.image?
         {
           original: '1280x1280>',
-          small: '250x250>',
+          small: '400x400>',
         }
       else
         {
           small: {
             convert_options: {
               output: {
-                vf: 'scale=\'min(250\, iw):min(250\, ih)\':force_original_aspect_ratio=decrease',
+                vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
               },
             },
             format: 'png',
diff --git a/app/models/tag.rb b/app/models/tag.rb
index e5b0511ae..77a73cce8 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -3,7 +3,7 @@
 class Tag < ApplicationRecord
   has_and_belongs_to_many :statuses
 
-  HASHTAG_RE = /(?:^|[^\/\w])#([[:word:]_]+)/i
+  HASHTAG_RE = /(?:^|[^\/\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i
 
   validates :name, presence: true, uniqueness: true
 
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 2f280e03f..5c04cfee4 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -8,7 +8,6 @@ class FavouriteService < BaseService
   def call(account, status)
     favourite = Favourite.create!(account: account, status: status)
 
-    HubPingWorker.perform_async(account.id)
     Pubsubhubbub::DistributionWorker.perform_async(favourite.stream_entry.id)
 
     if status.local?
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 423b833cf..ed9b62455 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -20,7 +20,6 @@ class FollowService < BaseService
 
     merge_into_timeline(target_account, source_account)
 
-    HubPingWorker.perform_async(source_account.id)
     Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id)
 
     follow
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 9e0ced129..d5204151b 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -16,7 +16,6 @@ class PostStatusService < BaseService
     process_hashtags_service.call(status)
 
     DistributionWorker.perform_async(status.id)
-    HubPingWorker.perform_async(account.id)
     Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
 
     status
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 39fdb4ea7..7d0c90d2f 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -9,7 +9,6 @@ class ReblogService < BaseService
     reblog = account.statuses.create!(reblog: reblogged_status, text: '')
 
     DistributionWorker.perform_async(reblog.id)
-    HubPingWorker.perform_async(account.id)
     Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
 
     if reblogged_status.local?
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index fa55e668e..836b8fdc5 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -13,7 +13,6 @@ class RemoveStatusService < BaseService
 
     return unless status.account.local?
 
-    HubPingWorker.perform_async(status.account.id)
     Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
   end
 
diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml
index 307d75c81..160a66710 100644
--- a/app/views/about/index.html.haml
+++ b/app/views/about/index.html.haml
@@ -1,6 +1,15 @@
 - content_for :page_title do
   = Rails.configuration.x.local_domain
 
+- content_for :header_tags do
+  %meta{ property: 'og:site_name', content: 'Mastodon' }/
+  %meta{ property: 'og:type', content: 'website' }/
+  %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/
+  %meta{ property: 'og:description', content: "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" }/
+  %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/
+  %meta{ property: 'og:image:width', content: '400' }/
+  %meta{ property: 'og:image:height', content: '400' }/
+
 .wrapper
   %h1
     = image_tag 'logo.png'
diff --git a/app/views/accounts/show.atom.ruby b/app/views/accounts/show.atom.ruby
index b2903d189..a22568396 100644
--- a/app/views/accounts/show.atom.ruby
+++ b/app/views/accounts/show.atom.ruby
@@ -15,7 +15,6 @@ Nokogiri::XML::Builder.new do |xml|
     link_alternate xml, TagManager.instance.url_for(@account)
     link_self      xml, account_url(@account, format: 'atom')
     link_hub       xml, api_push_url
-    link_hub       xml, Rails.configuration.x.hub_url
     link_salmon    xml, api_salmon_url(@account.id)
 
     @entries.each do |stream_entry|
diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl
index e035bc13c..f33b70ee5 100644
--- a/app/views/api/oembed/show.json.rabl
+++ b/app/views/api/oembed/show.json.rabl
@@ -9,6 +9,6 @@ node(:author_url) { |entry| account_url(entry.account) }
 node(:provider_name) { Rails.configuration.x.local_domain }
 node(:provider_url) { root_url }
 node(:cache_age) { 86_400 }
-node(:html, &:content)
+node(:html) { |entry| "<iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"width: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" height=\"#{@height}\" scrolling=\"no\"></iframe>" }
 node(:width) { @width }
-node(:height) { @height }
+node(:height) { nil }
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 498fae105..0adce05bf 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,4 +1,5 @@
 - content_for :header_tags do
+  %meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/
   = javascript_include_tag 'application'
 
 = react_component 'Mastodon', default_props, class: 'app-holder', prerender: false
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 87f98198c..7e28d27ec 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -9,7 +9,6 @@
     %link{:rel => "manifest", :href => "/manifest.json"}/
     %meta{:name => "msapplication-config", :content => "/browserconfig.xml"}/
     %meta{:name => "theme-color", :content => "#2b90d9"}/
-    %meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/
 
     %title
       = "#{yield(:page_title)} - " if content_for?(:page_title)
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
new file mode 100644
index 000000000..adbf0a287
--- /dev/null
+++ b/app/views/layouts/embedded.html.haml
@@ -0,0 +1,8 @@
+!!! 5
+%html{:lang => 'en'}
+  %head
+    %meta{:charset => 'utf-8'}/
+    = stylesheet_link_tag 'application', media: 'all'
+    = javascript_include_tag 'application_public'
+  %body.embed
+    = yield
diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml
new file mode 100644
index 000000000..d80ea46a0
--- /dev/null
+++ b/app/views/stream_entries/_content_spoiler.html.haml
@@ -0,0 +1,3 @@
+.media-spoiler
+  %span= t('stream_entries.sensitive_content')
+  %span= t('stream_entries.click_to_show')
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
new file mode 100644
index 000000000..94451d3bd
--- /dev/null
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -0,0 +1,36 @@
+.detailed-status.light
+  = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name', target: @external_links ? '_blank' : nil, rel: 'noopener' do
+    %div
+      %div.avatar
+        = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: ''
+    %span.display-name
+      %strong= display_name(status.account)
+      %span= acct(status.account)
+
+  .status__content= Formatter.instance.format(status)
+
+  - unless status.media_attachments.empty?
+    - if status.media_attachments.first.video?
+      .video-player
+        - if status.sensitive?
+          = render partial: 'stream_entries/content_spoiler'
+        %video{ src: status.media_attachments.first.file.url(:original), loop: true }
+    - else
+      .detailed-status__attachments
+        - if status.sensitive?
+          = render partial: 'stream_entries/content_spoiler'
+        - status.media_attachments.each do |media|
+          .media-item
+            = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener'
+
+  %div.detailed-status__meta
+    = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: @external_links ? '_blank' : nil, rel: 'noopener' do
+      %span= l(status.created_at)
+    ·
+    %span
+      = fa_icon('retweet')
+      %span= status.reblogs.count
+    ·
+    %span
+      = fa_icon('star')
+      %span= status.favourites.count
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
new file mode 100644
index 000000000..da3bc0ccb
--- /dev/null
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -0,0 +1,28 @@
+.status.light
+  .status__header
+    .status__meta
+      = link_to time_ago_in_words(status.created_at), TagManager.instance.url_for(status), class: 'status__relative-time', title: l(status.created_at), target: @external_links ? '_blank' : nil, rel: 'noopener'
+
+    = link_to TagManager.instance.url_for(status.account), class: 'status__display-name', target: @external_links ? '_blank' : nil, rel: 'noopener' do
+      .status__avatar
+        %div
+          = image_tag status.account.avatar(:original), width: 48, height: 48, alt: ''
+      %span.display-name
+        %strong= display_name(status.account)
+        %span= acct(status.account)
+
+  .status__content= Formatter.instance.format(status)
+
+  - unless status.media_attachments.empty?
+    .status__attachments
+      - if status.sensitive?
+        = render partial: 'stream_entries/content_spoiler'
+      - if status.media_attachments.first.video?
+        .video-item
+          = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener' do
+            .video-item__play
+              = fa_icon('play')
+      - else
+        - status.media_attachments.each do |media|
+          .media-item
+            = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener'
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
index 8169b8178..67cb06a83 100644
--- a/app/views/stream_entries/_status.html.haml
+++ b/app/views/stream_entries/_status.html.haml
@@ -1,7 +1,7 @@
 - include_threads ||= false
 - is_predecessor  ||= false
 - is_successor    ||= false
-- centered          = include_threads && !is_predecessor && !is_successor
+- centered        ||= include_threads && !is_predecessor && !is_successor
 
 - if status.reply? && include_threads
   = render partial: 'status', collection: @ancestors, as: :status, locals: { is_predecessor: true }
@@ -13,28 +13,7 @@
       Shared by
       = link_to display_name(status.account), TagManager.instance.url_for(status.account), class: 'name'
 
-  .entry__container
-    .avatar
-      = image_tag avatar_for_status_url(status)
-
-    .entry__container__container
-      .header
-        .header__left
-          = link_to TagManager.instance.url_for(proper_status(status).account), class: 'name' do
-            %strong= display_name(proper_status(status).account)
-            = "@#{proper_status(status).account.acct}"
-
-        .header__right
-          = link_to TagManager.instance.url_for(proper_status(status)), class: 'time' do
-            %span{ title: proper_status(status).created_at }
-              = relative_time(proper_status(status).created_at)
-
-      .content= Formatter.instance.format(proper_status(status))
-
-      - if (status.reblog? ? status.reblog : status).media_attachments.size > 0
-        %ul.media-attachments
-          - (status.reblog? ? status.reblog : status).media_attachments.each do |media|
-            %li.transparent-background= link_to '', media.file.url( :original), style: "background-image: url(#{media.file.url( :small)})", target: '_blank'
+  = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) }
 
 - if include_threads
   = render partial: 'status', collection: @descendants, as: :status, locals: { is_successor: true }
diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml
new file mode 100644
index 000000000..fd07fdd91
--- /dev/null
+++ b/app/views/stream_entries/embed.html.haml
@@ -0,0 +1,2 @@
+.activity-stream.activity-stream-headless
+  = render partial: @type, locals: { @type.to_sym => @stream_entry.activity, centered: true }
diff --git a/app/workers/hub_ping_worker.rb b/app/workers/hub_ping_worker.rb
deleted file mode 100644
index 14a151ba0..000000000
--- a/app/workers/hub_ping_worker.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class HubPingWorker
-  include Sidekiq::Worker
-  include RoutingHelper
-
-  def perform(account_id)
-    account = Account.find(account_id)
-    return unless account.local?
-    OStatus2::Publication.new(account_url(account, format: 'atom'), [Rails.configuration.x.hub_url]).publish
-  end
-end
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 4dc6985b7..e72063844 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -33,6 +33,7 @@ search:
 ignore_unused:
   - 'activerecord.attributes.*'
   - '{devise,will_paginate,doorkeeper}.*'
+  - '{datetime,time}.*'
   - 'simple_form.{yes,no}'
   - 'simple_form.{placeholders,hints,labels}.*'
   - 'simple_form.{error_notification,required}.:'
diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb
index c5723b2e9..faa9940b0 100644
--- a/config/initializers/ostatus.rb
+++ b/config/initializers/ostatus.rb
@@ -6,7 +6,6 @@ https = ENV['LOCAL_HTTPS'] == 'true'
 
 Rails.application.configure do
   config.x.local_domain = host
-  config.x.hub_url      = ENV.fetch('HUB_URL') { 'https://pubsubhubbub.superfeedr.com' }
   config.x.use_https    = https
   config.x.use_s3       = ENV['S3_ENABLED'] == 'true'
 
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 50a1f0e95..f58ce9a71 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -26,6 +26,20 @@ en:
     resend_confirmation: Resend confirmation instructions
     reset_password: Reset password
     set_new_password: Set new password
+  datetime:
+    distance_in_words:
+      about_x_hours: "%{count}h"
+      about_x_months: "%{count}mo"
+      about_x_years: "%{count}y"
+      almost_x_years: "%{count}y"
+      half_a_minute: Just now
+      less_than_x_minutes: "%{count}m"
+      less_than_x_seconds: Just now
+      over_x_years: "%{count}y"
+      x_days: "%{count}d"
+      x_minutes: "%{count}m"
+      x_months: "%{count}mo"
+      x_seconds: "%{count}s"
   generic:
     changes_saved_msg: Changes successfully saved!
     powered_by: powered by %{link}
@@ -53,8 +67,13 @@ en:
     edit_profile: Edit profile
     preferences: Preferences
   stream_entries:
+    click_to_show: Click to show
     favourited: favourited a post by
     is_now_following: is now following
+    sensitive_content: Sensitive content
+  time:
+    formats:
+      default: "%b %d, %Y, %H:%M"
   users:
     invalid_email: The e-mail address is invalid
   will_paginate:
diff --git a/config/routes.rb b/config/routes.rb
index 2d70bdcea..fd187dc42 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -25,7 +25,11 @@ Rails.application.routes.draw do
   }
 
   resources :accounts, path: 'users', only: [:show], param: :username do
-    resources :stream_entries, path: 'updates', only: [:show]
+    resources :stream_entries, path: 'updates', only: [:show] do
+      member do
+        get :embed
+      end
+    end
 
     member do
       get :followers
diff --git a/package.json b/package.json
index ab75c5be5..05663a729 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     "browserify-incremental": "^3.1.1",
     "chai": "^3.5.0",
     "chai-enzyme": "^0.5.2",
+    "css-loader": "^0.26.1",
     "emojione": "^2.2.6",
     "enzyme": "^2.4.1",
     "es6-promise": "^3.2.1",
@@ -25,6 +26,7 @@
     "intl": "^1.2.5",
     "jsdom": "^9.6.0",
     "mocha": "^3.1.1",
+    "node-sass": "^4.0.0",
     "react": "^15.3.2",
     "react-addons-perf": "^15.3.2",
     "react-addons-pure-render-mixin": "^15.3.1",
@@ -42,13 +44,14 @@
     "react-router": "^2.8.0",
     "react-router-scroll": "^0.3.2",
     "react-simple-dropdown": "^1.1.4",
+    "react-storybook-addon-intl": "^0.1.0",
+    "react-toggle": "^2.1.1",
     "redux": "^3.5.2",
     "redux-immutable": "^3.0.8",
     "redux-thunk": "^2.1.0",
     "reselect": "^2.5.4",
-    "sinon": "^1.17.6"
-  },
-  "dependencies": {
-    "react-toggle": "^2.1.1"
+    "sass-loader": "^4.0.2",
+    "sinon": "^1.17.6",
+    "style-loader": "^0.13.1"
   }
 }
diff --git a/public/500.html b/public/500.html
index d656269aa..915b890f1 100644
--- a/public/500.html
+++ b/public/500.html
@@ -20,6 +20,7 @@
       margin: 20px auto;
       margin-top: 50px;
       max-width: 600px;
+      width: 100%;
       height: auto;
     }
 
diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb
index 1346141fa..97d69ab7b 100644
--- a/spec/controllers/api/v1/follows_controller_spec.rb
+++ b/spec/controllers/api/v1/follows_controller_spec.rb
@@ -18,7 +18,6 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
       stub_request(:get,  "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
       stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
       stub_request(:post, "https://quitter.no/main/salmon/user/7477").to_return(:status => 200, :body => "", :headers => {})
-      stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
 
       post :create, params: { uri: 'gargron@quitter.no' }
     end
@@ -39,10 +38,6 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
       expect(a_request(:post, "https://quitter.no/main/salmon/user/7477")).to have_been_made
     end
 
-    it 'notifies own hub' do
-      expect(a_request(:post, "https://pubsubhubbub.superfeedr.com/")).to have_been_made
-    end
-
     it 'subscribes to remote hub' do
       expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made
     end
diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb
index f0a40fe91..5f89169e9 100644
--- a/spec/services/reblog_service_spec.rb
+++ b/spec/services/reblog_service_spec.rb
@@ -8,7 +8,6 @@ RSpec.describe ReblogService do
   subject { ReblogService.new }
 
   before do
-    stub_request(:post, Rails.configuration.x.hub_url)
     stub_request(:post, 'http://salmon.example.com')
 
     subject.(alice, status)
@@ -18,10 +17,6 @@ RSpec.describe ReblogService do
     expect(status.reblogs.count).to eq 1
   end
 
-  it 'pings PubSubHubbub hubs' do
-    expect(a_request(:post, Rails.configuration.x.hub_url)).to have_been_made
-  end
-
   it 'sends a Salmon slap for a remote reblog' do
     expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
   end
diff --git a/storybook/config.js b/storybook/config.js
index d9fde833c..4a111a8b9 100644
--- a/storybook/config.js
+++ b/storybook/config.js
@@ -1,8 +1,14 @@
-import { configure } from '@kadira/storybook';
+import { configure, setAddon } from '@kadira/storybook';
+import IntlAddon from 'react-storybook-addon-intl';
 import React from 'react';
 import { storiesOf, action } from '@kadira/storybook';
+import { addLocaleData } from 'react-intl';
+import en from 'react-intl/locale-data/en';
+import '../app/assets/stylesheets/components.scss'
+import './storybook.scss'
 
-import './storybook.css'
+setAddon(IntlAddon);
+addLocaleData(en);
 
 window.storiesOf = storiesOf;
 window.action    = action;
@@ -11,7 +17,7 @@ window.React     = React;
 function loadStories () {
   require('./stories/loading_indicator.story.jsx');
   require('./stories/button.story.jsx');
-  require('./stories/tabs_bar.story.jsx');
+  require('./stories/autosuggest_textarea.story.jsx');
 }
 
 configure(loadStories, module);
diff --git a/storybook/stories/autosuggest_textarea.story.jsx b/storybook/stories/autosuggest_textarea.story.jsx
new file mode 100644
index 000000000..7d84ff1e1
--- /dev/null
+++ b/storybook/stories/autosuggest_textarea.story.jsx
@@ -0,0 +1,6 @@
+import { storiesOf } from '@kadira/storybook';
+import AutosuggestTextarea from '../../app/assets/javascripts/components/components/autosuggest_textarea.jsx'
+
+storiesOf('AutosuggestTextarea', module)
+  .add('default state', () => <AutosuggestTextarea />)
+  .add('with text', () => <AutosuggestTextarea value='Hello' />)
diff --git a/storybook/stories/button.story.jsx b/storybook/stories/button.story.jsx
index fe6d57ad0..fc392abef 100644
--- a/storybook/stories/button.story.jsx
+++ b/storybook/stories/button.story.jsx
@@ -1,3 +1,4 @@
+import { storiesOf } from '@kadira/storybook';
 import Button from '../../app/assets/javascripts/components/components/button.jsx'
 
 storiesOf('Button', module)
diff --git a/storybook/stories/loading_indicator.story.jsx b/storybook/stories/loading_indicator.story.jsx
index d169e4f55..f4a961c4e 100644
--- a/storybook/stories/loading_indicator.story.jsx
+++ b/storybook/stories/loading_indicator.story.jsx
@@ -1,6 +1,6 @@
+import { storiesOf } from '@kadira/storybook';
 import LoadingIndicator from '../../app/assets/javascripts/components/components/loading_indicator.jsx'
+import { IntlProvider } from 'react-intl';
 
 storiesOf('LoadingIndicator', module)
-  .add('default state', () => (
-    <LoadingIndicator />
-  ));
+  .add('default state', () => <IntlProvider><LoadingIndicator /></IntlProvider>);
diff --git a/storybook/stories/tabs_bar.story.jsx b/storybook/stories/tabs_bar.story.jsx
deleted file mode 100644
index daaedca5a..000000000
--- a/storybook/stories/tabs_bar.story.jsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import TabsBar from '../../app/assets/javascripts/components/features/ui/components/tabs_bar.jsx'
-
-storiesOf('TabsBar', module)
-  .add('default state', () => (
-    <TabsBar />
-  ));
diff --git a/storybook/storybook.css b/storybook/storybook.css
deleted file mode 100644
index 3bda9e64c..000000000
--- a/storybook/storybook.css
+++ /dev/null
@@ -1,3 +0,0 @@
-#root {
-  padding: 4rem;
-}
diff --git a/storybook/storybook.scss b/storybook/storybook.scss
new file mode 100644
index 000000000..b0145f9bd
--- /dev/null
+++ b/storybook/storybook.scss
@@ -0,0 +1,15 @@
+@import url(https://fonts.googleapis.com/css?family=Roboto:400,500,400italic);
+@import url(https://fonts.googleapis.com/css?family=Roboto+Mono:400,500);
+
+#root {
+  font-family: 'Roboto', sans-serif;
+  background: #282c37;
+  font-size: 13px;
+  line-height: 18px;
+  font-weight: 400;
+  color: #fff;
+  padding-bottom: 140px;
+  text-rendering: optimizelegibility;
+  font-feature-settings: "kern";
+  padding: 4rem;
+}
diff --git a/storybook/webpack.config.js b/storybook/webpack.config.js
new file mode 100644
index 000000000..0ce563e1a
--- /dev/null
+++ b/storybook/webpack.config.js
@@ -0,0 +1,13 @@
+const path = require('path');
+
+module.exports = {
+  module: {
+    loaders: [
+      {
+        test: /.scss$/,
+        loaders: ["style", "css", "sass"],
+        include: path.resolve(__dirname, '../')
+      }
+    ]
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index 733ef7e92..f71a8ae10 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -231,6 +231,10 @@ array-filter@~0.0.0:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
 
+array-find-index@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
 array-flatten@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -242,6 +246,13 @@ array-includes@^3.0.2:
     define-properties "^1.1.2"
     es-abstract "^1.5.0"
 
+array-index@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9"
+  dependencies:
+    debug "^2.2.0"
+    es6-symbol "^3.0.2"
+
 array-map@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
@@ -302,6 +313,10 @@ async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
 
+async-foreach@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+
 async@^0.9.0:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@@ -310,6 +325,12 @@ async@^1.3.0, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
+async@^2.0.1:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
+  dependencies:
+    lodash "^4.14.0"
+
 async@~0.2.6:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -1244,14 +1265,33 @@ buffer@^4.1.0, buffer@^4.9.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
+builtin-modules@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
 builtin-status-codes@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz#6f22003baacf003ccd287afe6872151fddc58579"
 
+camelcase-keys@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+  dependencies:
+    camelcase "^2.0.0"
+    map-obj "^1.0.0"
+
 camelcase@^1.0.2:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
 
+camelcase@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
 caniuse-db@^1.0.30000539, caniuse-db@^1.0.30000540:
   version "1.0.30000554"
   resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000554.tgz#cd1dbe423d00b6203ba93f05973a476428dec919"
@@ -1347,6 +1387,14 @@ cliui@^2.1.0:
     right-align "^0.1.1"
     wordwrap "0.0.2"
 
+cliui@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+    wrap-ansi "^2.0.0"
+
 clone@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
@@ -1535,6 +1583,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
     create-hash "^1.1.0"
     inherits "^2.0.1"
 
+cross-spawn@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+  dependencies:
+    lru-cache "^4.0.1"
+    which "^1.2.9"
+
 cryptiles@2.x.x:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@@ -1585,6 +1640,23 @@ css-loader@0.25.0:
     postcss-modules-values "^1.1.0"
     source-list-map "^0.1.4"
 
+css-loader@^0.26.1:
+  version "0.26.1"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.26.1.tgz#2ba7f20131b93597496b3e9bb500785a49cd29ea"
+  dependencies:
+    babel-code-frame "^6.11.0"
+    css-selector-tokenizer "^0.7.0"
+    cssnano ">=2.6.1 <4"
+    loader-utils "~0.2.2"
+    lodash.camelcase "^4.3.0"
+    object-assign "^4.0.1"
+    postcss "^5.0.6"
+    postcss-modules-extract-imports "^1.0.0"
+    postcss-modules-local-by-default "^1.0.1"
+    postcss-modules-scope "^1.0.0"
+    postcss-modules-values "^1.1.0"
+    source-list-map "^0.1.4"
+
 css-select@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
@@ -1602,6 +1674,14 @@ css-selector-tokenizer@^0.6.0:
     fastparse "^1.1.1"
     regexpu-core "^1.0.0"
 
+css-selector-tokenizer@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
+  dependencies:
+    cssesc "^0.1.0"
+    fastparse "^1.1.1"
+    regexpu-core "^1.0.0"
+
 css-what@2.1:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
@@ -1664,6 +1744,18 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0":
   dependencies:
     cssom "0.3.x"
 
+currently-unhandled@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+  dependencies:
+    array-find-index "^1.0.1"
+
+d@^0.1.1, d@~0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
+  dependencies:
+    es5-ext "~0.10.2"
+
 dashdash@^1.12.0:
   version "1.14.0"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141"
@@ -1680,7 +1772,7 @@ debug@2.2.0, debug@^2.1.1, debug@^2.2.0, debug@~2.2.0:
   dependencies:
     ms "0.7.1"
 
-decamelize@^1.0.0, decamelize@^1.1.2:
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
 
@@ -1894,6 +1986,12 @@ errno@^0.1.3:
   dependencies:
     prr "~0.0.0"
 
+error-ex@^1.2.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9"
+  dependencies:
+    is-arrayish "^0.2.1"
+
 error-stack-parser@^1.3.6:
   version "1.3.6"
   resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-1.3.6.tgz#e0e73b93e417138d1cd7c0b746b1a4a14854c292"
@@ -1917,10 +2015,25 @@ es-to-primitive@^1.1.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.1"
 
+es5-ext@^0.10.7, es5-ext@~0.10.11, es5-ext@~0.10.2:
+  version "0.10.12"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047"
+  dependencies:
+    es6-iterator "2"
+    es6-symbol "~3.1"
+
 es5-shim@^4.5.9:
   version "4.5.9"
   resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.9.tgz#2a1e2b9e583ff5fed0c20a3ee2cbf3f75230a5c0"
 
+es6-iterator@2:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac"
+  dependencies:
+    d "^0.1.1"
+    es5-ext "^0.10.7"
+    es6-symbol "3"
+
 es6-promise@^3.2.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
@@ -1929,6 +2042,13 @@ es6-shim@^0.35.1:
   version "0.35.1"
   resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.1.tgz#a23524009005b031ab4a352ac196dfdfd1144ab7"
 
+es6-symbol@3, es6-symbol@^3.0.2, es6-symbol@~3.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
+  dependencies:
+    d "~0.1.1"
+    es5-ext "~0.10.11"
+
 escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -2205,6 +2325,12 @@ gauge@~2.6.0:
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
+gaze@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+  dependencies:
+    globule "^1.0.0"
+
 generate-function@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
@@ -2215,6 +2341,10 @@ generate-object-property@^1.1.0:
   dependencies:
     is-property "^1.0.0"
 
+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-stdin@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -2238,7 +2368,7 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob@7.0.5, glob@^7.0.0, glob@^7.0.5:
+glob@7.0.5, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5:
   version "7.0.5"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95"
   dependencies:
@@ -2259,10 +2389,29 @@ glob@^5.0.15:
     once "^1.3.0"
     path-is-absolute "^1.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"
+
 globals@^8.3.0:
   version "8.18.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-8.18.0.tgz#93d4a62bdcac38cfafafc47d6b034768cb0ffcb4"
 
+globule@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/globule/-/globule-1.1.0.tgz#c49352e4dc183d85893ee825385eb994bb6df45f"
+  dependencies:
+    glob "~7.1.1"
+    lodash "~4.16.4"
+    minimatch "~3.0.2"
+
 graceful-fs@^4.1.2:
   version "4.1.9"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.9.tgz#baacba37d19d11f9d146d3578bc99958c3787e29"
@@ -2347,6 +2496,10 @@ home-or-tmp@^1.0.0:
     os-tmpdir "^1.0.1"
     user-home "^1.1.1"
 
+hosted-git-info@^2.1.4:
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b"
+
 html-comment-regex@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
@@ -2430,6 +2583,16 @@ imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
 
+in-publish@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+
+indent-string@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+  dependencies:
+    repeating "^2.0.0"
+
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -2514,6 +2677,10 @@ invariant@2.x.x, invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant
   dependencies:
     loose-envify "^1.0.0"
 
+invert-kv@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
 ipaddr.js@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230"
@@ -2522,6 +2689,10 @@ is-absolute-url@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.0.0.tgz#9c4b20b0e5c0cbef9a479a367ede6f991679f359"
 
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
 is-binary-path@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@@ -2532,6 +2703,12 @@ is-buffer@^1.0.2, is-buffer@^1.1.0:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b"
 
+is-builtin-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+  dependencies:
+    builtin-modules "^1.0.0"
+
 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"
@@ -2651,6 +2828,10 @@ is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
 
+is-utf8@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
 isarray@0.0.1, isarray@~0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -2659,6 +2840,10 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
 
+isexe@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
+
 isobject@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-1.0.2.tgz#f0f9b8ce92dd540fa0740882e3835a2e022ec78a"
@@ -2835,6 +3020,12 @@ lazy-cache@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
 
+lcid@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+  dependencies:
+    invert-kv "^1.0.0"
+
 levn@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -2848,6 +3039,16 @@ lexical-scope@^1.2.0:
   dependencies:
     astw "^2.0.0"
 
+load-json-file@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+    strip-bom "^2.0.0"
+
 loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.15, loader-utils@^0.2.7, loader-utils@~0.2.2, loader-utils@~0.2.5:
   version "0.2.16"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
@@ -2915,12 +3116,24 @@ lodash.assign@^3.2.0:
     lodash._createassigner "^3.0.0"
     lodash.keys "^3.0.0"
 
+lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
+
 lodash.camelcase@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-3.0.1.tgz#932c8b87f8a4377897c67197533282f97aeac298"
   dependencies:
     lodash._createcompounder "^3.0.0"
 
+lodash.camelcase@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+
+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"
@@ -2947,6 +3160,10 @@ lodash.isarray@^3.0.0:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
 
+lodash.isarray@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403"
+
 lodash.keys@^3.0.0, lodash.keys@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -2959,6 +3176,10 @@ lodash.memoize@~3.0.3:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
 
+lodash.mergewith@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
+
 lodash.pick@^4.2.0, lodash.pick@^4.2.1:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
@@ -2973,7 +3194,7 @@ lodash.words@^3.0.0:
   dependencies:
     lodash._root "^3.0.0"
 
-lodash@^4.1.0, lodash@^4.13.1, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1:
+lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1, lodash@~4.16.4:
   version "4.16.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.4.tgz#01ce306b9bad1319f2a5528674f88297aeb70127"
 
@@ -2991,6 +3212,20 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0:
   dependencies:
     js-tokens "^1.0.1"
 
+loud-rejection@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+  dependencies:
+    currently-unhandled "^0.4.1"
+    signal-exit "^3.0.0"
+
+lru-cache@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
+  dependencies:
+    pseudomap "^1.0.1"
+    yallist "^2.0.0"
+
 macaddress@^0.2.8:
   version "0.2.8"
   resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
@@ -3003,6 +3238,10 @@ mantra-core@^1.6.1:
     react-komposer "^1.9.0"
     react-simple-di "^1.2.0"
 
+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"
+
 math-expression-evaluator@^1.2.14:
   version "1.2.14"
   resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.14.tgz#39511771ed9602405fba9affff17eb4d2a3843ab"
@@ -3024,6 +3263,21 @@ memory-fs@~0.3.0:
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
+meow@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+  dependencies:
+    camelcase-keys "^2.0.0"
+    decamelize "^1.1.2"
+    loud-rejection "^1.0.0"
+    map-obj "^1.0.1"
+    minimist "^1.1.3"
+    normalize-package-data "^2.3.4"
+    object-assign "^4.0.1"
+    read-pkg-up "^1.0.1"
+    redent "^1.0.0"
+    trim-newlines "^1.0.0"
+
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -3079,7 +3333,7 @@ minimalistic-assert@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
 
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2:
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@~3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
   dependencies:
@@ -3089,7 +3343,7 @@ minimist@0.0.8, minimist@~0.0.1:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
-minimist@^1.1.0, minimist@^1.2.0:
+minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
@@ -3142,7 +3396,7 @@ ms@0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
 
-nan@^2.3.0:
+nan@^2.3.0, nan@^2.3.2:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232"
 
@@ -3157,6 +3411,25 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
+node-gyp@^3.3.1:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.4.0.tgz#dda558393b3ecbbe24c9e6b8703c71194c63fa36"
+  dependencies:
+    fstream "^1.0.0"
+    glob "^7.0.3"
+    graceful-fs "^4.1.2"
+    minimatch "^3.0.2"
+    mkdirp "^0.5.0"
+    nopt "2 || 3"
+    npmlog "0 || 1 || 2 || 3"
+    osenv "0"
+    path-array "^1.0.0"
+    request "2"
+    rimraf "2"
+    semver "2.x || 3.x || 4 || 5"
+    tar "^2.0.0"
+    which "1"
+
 node-libs-browser@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-0.6.0.tgz#244806d44d319e048bc8607b5cc4eaf9a29d2e3c"
@@ -3199,16 +3472,48 @@ node-pre-gyp@^0.6.29:
     tar "~2.2.0"
     tar-pack "~3.1.0"
 
+node-sass@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.0.0.tgz#3208301ad5a6096de227f3fc4c3ce682b9816afc"
+  dependencies:
+    async-foreach "^0.1.3"
+    chalk "^1.1.1"
+    cross-spawn "^3.0.0"
+    gaze "^1.0.0"
+    get-stdin "^4.0.1"
+    glob "^7.0.3"
+    in-publish "^2.0.0"
+    lodash.assign "^4.2.0"
+    lodash.clonedeep "^4.3.2"
+    lodash.isarray "^4.0.0"
+    lodash.mergewith "^4.6.0"
+    meow "^3.7.0"
+    mkdirp "^0.5.1"
+    nan "^2.3.2"
+    node-gyp "^3.3.1"
+    npmlog "^4.0.0"
+    request "^2.61.0"
+    sass-graph "^2.1.1"
+
 node-uuid@~1.4.7:
   version "1.4.7"
   resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f"
 
-nopt@~3.0.1:
+"nopt@2 || 3", nopt@~3.0.1:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
   dependencies:
     abbrev "1"
 
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+  version "2.3.5"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
+  dependencies:
+    hosted-git-info "^2.1.4"
+    is-builtin-module "^1.0.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
 normalize-path@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
@@ -3226,7 +3531,16 @@ normalize-url@^1.4.0:
     query-string "^4.1.0"
     sort-keys "^1.0.0"
 
-npmlog@4.x:
+"npmlog@0 || 1 || 2 || 3":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-3.1.2.tgz#2d46fa874337af9498a2f12bb43d8d0be4a36873"
+  dependencies:
+    are-we-there-yet "~1.1.2"
+    console-control-strings "~1.1.0"
+    gauge "~2.6.0"
+    set-blocking "~2.0.0"
+
+npmlog@4.x, npmlog@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.0.tgz#e094503961c70c1774eb76692080e8d578a9f88f"
   dependencies:
@@ -3357,11 +3671,17 @@ os-homedir@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
 
+os-locale@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+  dependencies:
+    lcid "^1.0.0"
+
 os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
-osenv@^0.1.0:
+osenv@0, osenv@^0.1.0:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.3.tgz#83cf05c6d6458fc4d5ac6362ea325d92f2754217"
   dependencies:
@@ -3397,6 +3717,12 @@ parse-glob@^3.0.4:
     is-extglob "^1.0.0"
     is-glob "^2.0.0"
 
+parse-json@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+  dependencies:
+    error-ex "^1.2.0"
+
 parse5@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
@@ -3405,6 +3731,12 @@ parseurl@~1.3.0, parseurl@~1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
 
+path-array@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-array/-/path-array-1.0.1.tgz#7e2f0f35f07a2015122b868b7eac0eb2c4fec271"
+  dependencies:
+    array-index "^1.0.0"
+
 path-browserify@0.0.0, path-browserify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
@@ -3431,6 +3763,14 @@ path-to-regexp@0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
 
+path-type@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+  dependencies:
+    graceful-fs "^4.1.2"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
 pbkdf2-compat@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz#b6e0c8fa99494d94e0511575802a59a5c142f288"
@@ -3445,6 +3785,10 @@ performance-now@^0.2.0, performance-now@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
 
+pify@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
 pinkie-promise@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
@@ -3742,6 +4086,10 @@ prr@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
 
+pseudomap@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
 public-encrypt@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
@@ -3994,6 +4342,10 @@ react-simple-dropdown@^1.1.4:
   dependencies:
     classnames "^2.1.2"
 
+react-storybook-addon-intl@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/react-storybook-addon-intl/-/react-storybook-addon-intl-0.1.0.tgz#4d46c9e6c7be0ad4e4f7de72d907ec764743dee8"
+
 react-themeable@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e"
@@ -4021,6 +4373,21 @@ read-only-stream@^2.0.0:
   dependencies:
     readable-stream "^2.0.2"
 
+read-pkg-up@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+  dependencies:
+    find-up "^1.0.0"
+    read-pkg "^1.0.0"
+
+read-pkg@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+  dependencies:
+    load-json-file "^1.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^1.0.0"
+
 readable-stream@1.1, readable-stream@^1.0.27-1, readable-stream@^1.1.13:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -4076,6 +4443,13 @@ redbox-react@^1.2.2:
     object-assign "^4.0.1"
     react-dom "^0.14.0 || ^15.0.0"
 
+redent@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+  dependencies:
+    indent-string "^2.1.0"
+    strip-indent "^1.0.1"
+
 reduce-css-calc@^1.2.6:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
@@ -4164,7 +4538,13 @@ repeating@^1.1.0:
   dependencies:
     is-finite "^1.0.0"
 
-request@2.x, request@^2.55.0, request@^2.74.0:
+repeating@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+  dependencies:
+    is-finite "^1.0.0"
+
+request@2, request@2.x, request@^2.55.0, request@^2.61.0, request@^2.74.0:
   version "2.75.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93"
   dependencies:
@@ -4190,6 +4570,14 @@ request@2.x, request@^2.55.0, request@^2.74.0:
     tough-cookie "~2.3.0"
     tunnel-agent "~0.4.1"
 
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
 reselect@^2.5.4:
   version "2.5.4"
   resolved "https://registry.yarnpkg.com/reselect/-/reselect-2.5.4.tgz#b7d23fdf00b83fa7ad0279546f8dbbbd765c7047"
@@ -4222,6 +4610,22 @@ samsam@1.1.2, samsam@~1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
 
+sass-graph@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.1.2.tgz#965104be23e8103cb7e5f710df65935b317da57b"
+  dependencies:
+    glob "^7.0.0"
+    lodash "^4.0.0"
+    yargs "^4.7.1"
+
+sass-loader@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-4.0.2.tgz#a616eb770366543e64f547c8630f39c4da75f15d"
+  dependencies:
+    async "^2.0.1"
+    loader-utils "^0.2.15"
+    object-assign "^4.1.0"
+
 sax@^1.1.4, sax@~1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
@@ -4237,7 +4641,7 @@ section-iterator@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a"
 
-semver@~5.3.0:
+"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
 
@@ -4277,7 +4681,7 @@ serve-static@~1.11.1:
     parseurl "~1.3.1"
     send "0.14.1"
 
-set-blocking@~2.0.0:
+set-blocking@^2.0.0, set-blocking@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
 
@@ -4402,6 +4806,20 @@ source-map@~0.4.1:
   dependencies:
     amdefine ">=0.0.4"
 
+spdx-correct@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+  dependencies:
+    spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -4527,11 +4945,23 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   dependencies:
     ansi-regex "^2.0.0"
 
+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-indent@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+  dependencies:
+    get-stdin "^4.0.1"
+
 strip-json-comments@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
 
-style-loader@0.13.1:
+style-loader@0.13.1, style-loader@^0.13.1:
   version "0.13.1"
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.1.tgz#468280efbc0473023cd3a6cd56e33b5a1d7fc3a9"
   dependencies:
@@ -4596,7 +5026,7 @@ tar-pack@~3.1.0:
     tar "~2.2.1"
     uid-number "~0.0.6"
 
-tar@~2.2.0, tar@~2.2.1:
+tar@^2.0.0, tar@~2.2.0, tar@~2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
   dependencies:
@@ -4641,6 +5071,10 @@ 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"
+
 tty-browserify@0.0.0, tty-browserify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -4764,6 +5198,13 @@ uuid@^2.0.1, uuid@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
 
+validate-npm-package-license@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+  dependencies:
+    spdx-correct "~1.0.0"
+    spdx-expression-parse "~1.0.0"
+
 vary@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140"
@@ -4878,6 +5319,16 @@ whet.extend@~0.9.9:
   version "0.9.9"
   resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
 
+which-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which@1, which@^1.2.9:
+  version "1.2.12"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
+  dependencies:
+    isexe "^1.1.1"
+
 wide-align@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad"
@@ -4888,6 +5339,10 @@ window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
+window-size@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
+
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
@@ -4900,6 +5355,13 @@ wordwrap@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
 
+wrap-ansi@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -4926,6 +5388,40 @@ xtend@^4.0.0, xtend@~4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 
+y18n@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4"
+
+yargs-parser@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4"
+  dependencies:
+    camelcase "^3.0.0"
+    lodash.assign "^4.0.6"
+
+yargs@^4.7.1:
+  version "4.8.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0"
+  dependencies:
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    get-caller-file "^1.0.1"
+    lodash.assign "^4.0.3"
+    os-locale "^1.4.0"
+    read-pkg-up "^1.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^1.0.1"
+    which-module "^1.0.0"
+    window-size "^0.2.0"
+    y18n "^3.2.1"
+    yargs-parser "^2.4.1"
+
 yargs@~3.10.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"