about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/mastodon/actions/interactions.js78
-rw-r--r--app/javascript/mastodon/actions/streaming.js94
-rw-r--r--app/javascript/mastodon/components/account.js12
-rw-r--r--app/javascript/mastodon/components/column.js2
-rw-r--r--app/javascript/mastodon/components/intersection_observer_article.js122
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js179
-rw-r--r--app/javascript/mastodon/components/status.js114
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js29
-rw-r--r--app/javascript/mastodon/components/status_list.js156
-rw-r--r--app/javascript/mastodon/containers/account_container.js2
-rw-r--r--app/javascript/mastodon/containers/compose_container.js39
-rw-r--r--app/javascript/mastodon/containers/mastodon.js82
-rw-r--r--app/javascript/mastodon/containers/status_container.js14
-rw-r--r--app/javascript/mastodon/features/account/components/header.js4
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js2
-rw-r--r--app/javascript/mastodon/features/community_timeline/index.js50
-rw-r--r--app/javascript/mastodon/features/favourited_statuses/index.js5
-rw-r--r--app/javascript/mastodon/features/hashtag_timeline/index.js35
-rw-r--r--app/javascript/mastodon/features/notifications/components/notification.js10
-rw-r--r--app/javascript/mastodon/features/notifications/index.js104
-rw-r--r--app/javascript/mastodon/features/public_timeline/index.js50
-rw-r--r--app/javascript/mastodon/features/standalone/compose/index.js18
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js23
-rw-r--r--app/javascript/mastodon/features/status/components/card.js9
-rw-r--r--app/javascript/mastodon/features/status/index.js16
-rw-r--r--app/javascript/mastodon/features/ui/components/column.js11
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js35
-rw-r--r--app/javascript/mastodon/features/ui/components/embed_modal.js84
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/onboarding_modal.js2
-rw-r--r--app/javascript/mastodon/features/ui/containers/columns_area_container.js2
-rw-r--r--app/javascript/mastodon/features/ui/index.js19
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js4
-rw-r--r--app/javascript/mastodon/locales/ar.json7
-rw-r--r--app/javascript/mastodon/locales/bg.json5
-rw-r--r--app/javascript/mastodon/locales/ca.json5
-rw-r--r--app/javascript/mastodon/locales/de.json157
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json43
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/locales/eo.json5
-rw-r--r--app/javascript/mastodon/locales/es.json5
-rw-r--r--app/javascript/mastodon/locales/fa.json5
-rw-r--r--app/javascript/mastodon/locales/fi.json5
-rw-r--r--app/javascript/mastodon/locales/fr.json21
-rw-r--r--app/javascript/mastodon/locales/he.json5
-rw-r--r--app/javascript/mastodon/locales/hr.json5
-rw-r--r--app/javascript/mastodon/locales/hu.json5
-rw-r--r--app/javascript/mastodon/locales/id.json5
-rw-r--r--app/javascript/mastodon/locales/io.json5
-rw-r--r--app/javascript/mastodon/locales/it.json5
-rw-r--r--app/javascript/mastodon/locales/ja.json7
-rw-r--r--app/javascript/mastodon/locales/ko.json5
-rw-r--r--app/javascript/mastodon/locales/nl.json5
-rw-r--r--app/javascript/mastodon/locales/no.json5
-rw-r--r--app/javascript/mastodon/locales/oc.json47
-rw-r--r--app/javascript/mastodon/locales/pl.json41
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json81
-rw-r--r--app/javascript/mastodon/locales/pt.json5
-rw-r--r--app/javascript/mastodon/locales/ru.json43
-rw-r--r--app/javascript/mastodon/locales/th.json5
-rw-r--r--app/javascript/mastodon/locales/tr.json5
-rw-r--r--app/javascript/mastodon/locales/uk.json5
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json33
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json5
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json5
-rw-r--r--app/javascript/mastodon/reducers/compose.js12
-rw-r--r--app/javascript/mastodon/reducers/status_lists.js20
-rw-r--r--app/javascript/mastodon/reducers/statuses.js4
-rw-r--r--app/javascript/mastodon/scroll.js2
-rw-r--r--app/javascript/mastodon/service_worker/web_push_notifications.js26
-rw-r--r--app/javascript/mastodon/web_push_subscription.js4
-rw-r--r--app/javascript/packs/public.js12
-rw-r--r--app/javascript/packs/share.js24
-rw-r--r--app/javascript/styles/about.scss867
-rw-r--r--app/javascript/styles/accounts.scss343
-rw-r--r--app/javascript/styles/admin.scss59
-rw-r--r--app/javascript/styles/basics.scss36
-rw-r--r--app/javascript/styles/components.scss97
-rw-r--r--app/javascript/styles/containers.scss22
-rw-r--r--app/javascript/styles/forms.scss47
-rw-r--r--app/javascript/styles/landing_strip.scss13
-rw-r--r--app/javascript/styles/rtl.scss129
-rw-r--r--app/javascript/styles/stream_entries.scss47
-rw-r--r--app/javascript/styles/tables.scss2
84 files changed, 2362 insertions, 1404 deletions
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 36eec4934..7b5f4bd9c 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -24,6 +24,14 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
 export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
 export const FAVOURITES_FETCH_FAIL    = 'FAVOURITES_FETCH_FAIL';
 
+export const PIN_REQUEST = 'PIN_REQUEST';
+export const PIN_SUCCESS = 'PIN_SUCCESS';
+export const PIN_FAIL    = 'PIN_FAIL';
+
+export const UNPIN_REQUEST = 'UNPIN_REQUEST';
+export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
+export const UNPIN_FAIL    = 'UNPIN_FAIL';
+
 export function reblog(status) {
   return function (dispatch, getState) {
     dispatch(reblogRequest(status));
@@ -233,3 +241,73 @@ export function fetchFavouritesFail(id, error) {
     error,
   };
 };
+
+export function pin(status) {
+  return (dispatch, getState) => {
+    dispatch(pinRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
+      dispatch(pinSuccess(status, response.data));
+    }).catch(error => {
+      dispatch(pinFail(status, error));
+    });
+  };
+};
+
+export function pinRequest(status) {
+  return {
+    type: PIN_REQUEST,
+    status,
+  };
+};
+
+export function pinSuccess(status, response) {
+  return {
+    type: PIN_SUCCESS,
+    status,
+    response,
+  };
+};
+
+export function pinFail(status, error) {
+  return {
+    type: PIN_FAIL,
+    status,
+    error,
+  };
+};
+
+export function unpin (status) {
+  return (dispatch, getState) => {
+    dispatch(unpinRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
+      dispatch(unpinSuccess(status, response.data));
+    }).catch(error => {
+      dispatch(unpinFail(status, error));
+    });
+  };
+};
+
+export function unpinRequest(status) {
+  return {
+    type: UNPIN_REQUEST,
+    status,
+  };
+};
+
+export function unpinSuccess(status, response) {
+  return {
+    type: UNPIN_SUCCESS,
+    status,
+    response,
+  };
+};
+
+export function unpinFail(status, error) {
+  return {
+    type: UNPIN_FAIL,
+    status,
+    error,
+  };
+};
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
new file mode 100644
index 000000000..7802694a3
--- /dev/null
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -0,0 +1,94 @@
+import createStream from '../stream';
+import {
+  updateTimeline,
+  deleteFromTimelines,
+  refreshHomeTimeline,
+  connectTimeline,
+  disconnectTimeline,
+} from './timelines';
+import { updateNotifications, refreshNotifications } from './notifications';
+import { getLocale } from '../locales';
+
+const { messages } = getLocale();
+
+export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
+  return (dispatch, getState) => {
+    const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
+    const accessToken = getState().getIn(['meta', 'access_token']);
+    const locale = getState().getIn(['meta', 'locale']);
+    let polling = null;
+
+    const setupPolling = () => {
+      polling = setInterval(() => {
+        pollingRefresh(dispatch);
+      }, 20000);
+    };
+
+    const clearPolling = () => {
+      if (polling) {
+        clearInterval(polling);
+        polling = null;
+      }
+    };
+
+    const subscription = createStream(streamingAPIBaseURL, accessToken, path, {
+
+      connected () {
+        if (pollingRefresh) {
+          clearPolling();
+        }
+        dispatch(connectTimeline(timelineId));
+      },
+
+      disconnected () {
+        if (pollingRefresh) {
+          setupPolling();
+        }
+        dispatch(disconnectTimeline(timelineId));
+      },
+
+      received (data) {
+        switch(data.event) {
+        case 'update':
+          dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
+          break;
+        case 'delete':
+          dispatch(deleteFromTimelines(data.payload));
+          break;
+        case 'notification':
+          dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
+          break;
+        }
+      },
+
+      reconnected () {
+        if (pollingRefresh) {
+          clearPolling();
+          pollingRefresh(dispatch);
+        }
+        dispatch(connectTimeline(timelineId));
+      },
+
+    });
+
+    const disconnect = () => {
+      if (subscription) {
+        subscription.close();
+      }
+      clearPolling();
+    };
+
+    return disconnect;
+  };
+}
+
+function refreshHomeTimelineAndNotification (dispatch) {
+  dispatch(refreshHomeTimeline());
+  dispatch(refreshNotifications());
+}
+
+export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
+export const connectCommunityStream = () => connectTimelineStream('community', 'public:local');
+export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
+export const connectPublicStream = () => connectTimelineStream('public', 'public');
+export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 69cc63d10..6456c12ba 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -26,6 +26,7 @@ export default class Account extends ImmutablePureComponent {
     onBlock: PropTypes.func.isRequired,
     onMute: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    hidden: PropTypes.bool,
   };
 
   handleFollow = () => {
@@ -41,12 +42,21 @@ export default class Account extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, me, intl } = this.props;
+    const { account, me, intl, hidden } = this.props;
 
     if (!account) {
       return <div />;
     }
 
+    if (hidden) {
+      return (
+        <div>
+          {account.get('display_name')}
+          {account.get('username')}
+        </div>
+      );
+    }
+
     let buttons;
 
     if (account.get('id') !== me && account.get('relationship', null) !== null) {
diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js
index 103fcd495..168f2da9c 100644
--- a/app/javascript/mastodon/components/column.js
+++ b/app/javascript/mastodon/components/column.js
@@ -33,7 +33,7 @@ export default class Column extends React.PureComponent {
   }
 
   componentDidMount () {
-    this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents ? { passive: true } : false);
+    this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
   }
 
   componentWillUnmount () {
diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js
new file mode 100644
index 000000000..347767818
--- /dev/null
+++ b/app/javascript/mastodon/components/intersection_observer_article.js
@@ -0,0 +1,122 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
+import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
+
+export default class IntersectionObserverArticle extends ImmutablePureComponent {
+
+  static propTypes = {
+    intersectionObserverWrapper: PropTypes.object,
+    id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    children: PropTypes.node,
+  };
+
+  state = {
+    isHidden: false, // set to true in requestIdleCallback to trigger un-render
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    if (!nextState.isIntersecting && nextState.isHidden) {
+      // It's only if we're not intersecting (i.e. offscreen) and isHidden is true
+      // that either "isIntersecting" or "isHidden" matter, and then they're
+      // the only things that matter (and updated ARIA attributes).
+      return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
+    } else if (nextState.isIntersecting && !this.state.isIntersecting) {
+      // If we're going from a non-intersecting state to an intersecting state,
+      // (i.e. offscreen to onscreen), then we definitely need to re-render
+      return true;
+    }
+    // Otherwise, diff based on "updateOnProps" and "updateOnStates"
+    return super.shouldComponentUpdate(nextProps, nextState);
+  }
+
+  componentDidMount () {
+    if (!this.props.intersectionObserverWrapper) {
+      // TODO: enable IntersectionObserver optimization for notification statuses.
+      // These are managed in notifications/index.js rather than status_list.js
+      return;
+    }
+    this.props.intersectionObserverWrapper.observe(
+      this.props.id,
+      this.node,
+      this.handleIntersection
+    );
+
+    this.componentMounted = true;
+  }
+
+  componentWillUnmount () {
+    if (this.props.intersectionObserverWrapper) {
+      this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
+    }
+
+    this.componentMounted = false;
+  }
+
+  handleIntersection = (entry) => {
+    if (this.node && this.node.children.length !== 0) {
+      // save the height of the fully-rendered element
+      this.height = getRectFromEntry(entry).height;
+
+      if (this.props.onHeightChange) {
+        this.props.onHeightChange(this.props.status, this.height);
+      }
+    }
+
+    this.setState((prevState) => {
+      if (prevState.isIntersecting && !entry.isIntersecting) {
+        scheduleIdleTask(this.hideIfNotIntersecting);
+      }
+      return {
+        isIntersecting: entry.isIntersecting,
+        isHidden: false,
+      };
+    });
+  }
+
+  hideIfNotIntersecting = () => {
+    if (!this.componentMounted) {
+      return;
+    }
+
+    // When the browser gets a chance, test if we're still not intersecting,
+    // and if so, set our isHidden to true to trigger an unrender. The point of
+    // this is to save DOM nodes and avoid using up too much memory.
+    // See: https://github.com/tootsuite/mastodon/issues/2900
+    this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
+  }
+
+  handleRef = (node) => {
+    this.node = node;
+  }
+
+  render () {
+    const { children, id, index, listLength } = this.props;
+    const { isIntersecting, isHidden } = this.state;
+
+    if (!isIntersecting && isHidden) {
+      return (
+        <article
+          ref={this.handleRef}
+          aria-posinset={index}
+          aria-setsize={listLength}
+          style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}
+          data-id={id}
+          tabIndex='0'
+        >
+          {children && React.cloneElement(children, { hidden: true })}
+        </article>
+      );
+    }
+
+    return (
+      <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
+        {children && React.cloneElement(children, { hidden: false })}
+      </article>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
new file mode 100644
index 000000000..1a122dbe5
--- /dev/null
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -0,0 +1,179 @@
+import React, { PureComponent } from 'react';
+import { ScrollContainer } from 'react-router-scroll';
+import PropTypes from 'prop-types';
+import IntersectionObserverArticle from './intersection_observer_article';
+import LoadMore from './load_more';
+import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
+import { throttle } from 'lodash';
+
+export default class ScrollableList extends PureComponent {
+
+  static propTypes = {
+    scrollKey: PropTypes.string.isRequired,
+    onScrollToBottom: PropTypes.func,
+    onScrollToTop: PropTypes.func,
+    onScroll: PropTypes.func,
+    trackScroll: PropTypes.bool,
+    shouldUpdateScroll: PropTypes.func,
+    isLoading: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    prepend: PropTypes.node,
+    emptyMessage: PropTypes.node,
+    children: PropTypes.node,
+  };
+
+  static defaultProps = {
+    trackScroll: true,
+  };
+
+  intersectionObserverWrapper = new IntersectionObserverWrapper();
+
+  handleScroll = throttle(() => {
+    if (this.node) {
+      const { scrollTop, scrollHeight, clientHeight } = this.node;
+      const offset = scrollHeight - scrollTop - clientHeight;
+      this._oldScrollPosition = scrollHeight - scrollTop;
+
+      if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
+        this.props.onScrollToBottom();
+      } else if (scrollTop < 100 && this.props.onScrollToTop) {
+        this.props.onScrollToTop();
+      } else if (this.props.onScroll) {
+        this.props.onScroll();
+      }
+    }
+  }, 150, {
+    trailing: true,
+  });
+
+  componentDidMount () {
+    this.attachScrollListener();
+    this.attachIntersectionObserver();
+
+    // Handle initial scroll posiiton
+    this.handleScroll();
+  }
+
+  componentDidUpdate (prevProps) {
+    // Reset the scroll position when a new child comes in in order not to
+    // jerk the scrollbar around if you're already scrolled down the page.
+    if (React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this._oldScrollPosition && this.node.scrollTop > 0) {
+      if (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) {
+        const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
+        if (this.node.scrollTop !== newScrollTop) {
+          this.node.scrollTop = newScrollTop;
+        }
+      } else {
+        this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
+      }
+    }
+  }
+
+  componentWillUnmount () {
+    this.detachScrollListener();
+    this.detachIntersectionObserver();
+  }
+
+  attachIntersectionObserver () {
+    this.intersectionObserverWrapper.connect({
+      root: this.node,
+      rootMargin: '300% 0px',
+    });
+  }
+
+  detachIntersectionObserver () {
+    this.intersectionObserverWrapper.disconnect();
+  }
+
+  attachScrollListener () {
+    this.node.addEventListener('scroll', this.handleScroll);
+  }
+
+  detachScrollListener () {
+    this.node.removeEventListener('scroll', this.handleScroll);
+  }
+
+  getFirstChildKey (props) {
+    const { children } = props;
+    const firstChild = Array.isArray(children) ? children[0] : children;
+    return firstChild && firstChild.key;
+  }
+
+  setRef = (c) => {
+    this.node = c;
+  }
+
+  handleLoadMore = (e) => {
+    e.preventDefault();
+    this.props.onScrollToBottom();
+  }
+
+  handleKeyDown = (e) => {
+    if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
+      const article = (() => {
+        switch (e.key) {
+        case 'PageDown':
+          return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
+        case 'PageUp':
+          return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
+        case 'End':
+          return this.node.querySelector('[role="feed"] > article:last-of-type');
+        case 'Home':
+          return this.node.querySelector('[role="feed"] > article:first-of-type');
+        default:
+          return null;
+        }
+      })();
+
+
+      if (article) {
+        e.preventDefault();
+        article.focus();
+        article.scrollIntoView();
+      }
+    }
+  }
+
+  render () {
+    const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
+    const childrenCount = React.Children.count(children);
+
+    const loadMore     = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />;
+    let scrollableArea = null;
+
+    if (isLoading || childrenCount > 0 || !emptyMessage) {
+      scrollableArea = (
+        <div className='scrollable' ref={this.setRef}>
+          <div role='feed' className='item-list' onKeyDown={this.handleKeyDown}>
+            {prepend}
+
+            {React.Children.map(this.props.children, (child, index) => (
+              <IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}>
+                {child}
+              </IntersectionObserverArticle>
+            ))}
+
+            {loadMore}
+          </div>
+        </div>
+      );
+    } else {
+      scrollableArea = (
+        <div className='empty-column-indicator' ref={this.setRef}>
+          {emptyMessage}
+        </div>
+      );
+    }
+
+    if (trackScroll) {
+      return (
+        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
+          {scrollableArea}
+        </ScrollContainer>
+      );
+    } else {
+      return scrollableArea;
+    }
+  }
+
+}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 7468957d3..b8617018d 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -12,13 +12,11 @@ import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
 import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
 
 // We use the component (and not the container) since we do not want
 // to use the progress bar to show download progress
 import Bundle from '../features/ui/components/bundle';
-import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
 
 export default class Status extends ImmutablePureComponent {
 
@@ -29,27 +27,25 @@ export default class Status extends ImmutablePureComponent {
   static propTypes = {
     status: ImmutablePropTypes.map,
     account: ImmutablePropTypes.map,
-    wrapped: PropTypes.bool,
     onReply: PropTypes.func,
     onFavourite: PropTypes.func,
     onReblog: PropTypes.func,
     onDelete: PropTypes.func,
+    onPin: PropTypes.func,
     onOpenMedia: PropTypes.func,
     onOpenVideo: PropTypes.func,
     onBlock: PropTypes.func,
+    onEmbed: PropTypes.func,
     onHeightChange: PropTypes.func,
     me: PropTypes.number,
     boostModal: PropTypes.bool,
     autoPlayGif: PropTypes.bool,
     muted: PropTypes.bool,
-    intersectionObserverWrapper: PropTypes.object,
-    index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-    listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    hidden: PropTypes.bool,
   };
 
   state = {
     isExpanded: false,
-    isHidden: false, // set to true in requestIdleCallback to trigger un-render
   }
 
   // Avoid checking props that are functions (and whose equality will always
@@ -57,91 +53,15 @@ export default class Status extends ImmutablePureComponent {
   updateOnProps = [
     'status',
     'account',
-    'wrapped',
     'me',
     'boostModal',
     'autoPlayGif',
     'muted',
-    'listLength',
+    'hidden',
   ]
 
   updateOnStates = ['isExpanded']
 
-  shouldComponentUpdate (nextProps, nextState) {
-    if (!nextState.isIntersecting && nextState.isHidden) {
-      // It's only if we're not intersecting (i.e. offscreen) and isHidden is true
-      // that either "isIntersecting" or "isHidden" matter, and then they're
-      // the only things that matter (and updated ARIA attributes).
-      return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
-    } else if (nextState.isIntersecting && !this.state.isIntersecting) {
-      // If we're going from a non-intersecting state to an intersecting state,
-      // (i.e. offscreen to onscreen), then we definitely need to re-render
-      return true;
-    }
-    // Otherwise, diff based on "updateOnProps" and "updateOnStates"
-    return super.shouldComponentUpdate(nextProps, nextState);
-  }
-
-  componentDidMount () {
-    if (!this.props.intersectionObserverWrapper) {
-      // TODO: enable IntersectionObserver optimization for notification statuses.
-      // These are managed in notifications/index.js rather than status_list.js
-      return;
-    }
-    this.props.intersectionObserverWrapper.observe(
-      this.props.id,
-      this.node,
-      this.handleIntersection
-    );
-
-    this.componentMounted = true;
-  }
-
-  componentWillUnmount () {
-    if (this.props.intersectionObserverWrapper) {
-      this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
-    }
-
-    this.componentMounted = false;
-  }
-
-  handleIntersection = (entry) => {
-    if (this.node && this.node.children.length !== 0) {
-      // save the height of the fully-rendered element
-      this.height = getRectFromEntry(entry).height;
-
-      if (this.props.onHeightChange) {
-        this.props.onHeightChange(this.props.status, this.height);
-      }
-    }
-
-    this.setState((prevState) => {
-      if (prevState.isIntersecting && !entry.isIntersecting) {
-        scheduleIdleTask(this.hideIfNotIntersecting);
-      }
-      return {
-        isIntersecting: entry.isIntersecting,
-        isHidden: false,
-      };
-    });
-  }
-
-  hideIfNotIntersecting = () => {
-    if (!this.componentMounted) {
-      return;
-    }
-
-    // When the browser gets a chance, test if we're still not intersecting,
-    // and if so, set our isHidden to true to trigger an unrender. The point of
-    // this is to save DOM nodes and avoid using up too much memory.
-    // See: https://github.com/tootsuite/mastodon/issues/2900
-    this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
-  }
-
-  handleRef = (node) => {
-    this.node = node;
-  }
-
   handleClick = () => {
     if (!this.context.router) {
       return;
@@ -175,25 +95,19 @@ export default class Status extends ImmutablePureComponent {
     let media = null;
     let statusAvatar;
 
-    // Exclude intersectionObserverWrapper from `other` variable
-    // because intersection is managed in here.
-    const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props;
-    const { isExpanded, isIntersecting, isHidden } = this.state;
+    const { status, account, hidden, ...other } = this.props;
+    const { isExpanded } = this.state;
 
     if (status === null) {
       return null;
     }
 
-    const hasIntersectionObserverWrapper = !!this.props.intersectionObserverWrapper;
-    const isHiddenForSure = isIntersecting === false && isHidden;
-    const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status.has('height');
-
-    if (hasIntersectionObserverWrapper && (isHiddenForSure || visibilityUnknownButHeightIsCached)) {
+    if (hidden) {
       return (
-        <article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height || status.get('height')}px`, opacity: 0, overflow: 'hidden' }}>
+        <div>
           {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
           {status.get('content')}
-        </article>
+        </div>
       );
     }
 
@@ -201,14 +115,14 @@ export default class Status extends ImmutablePureComponent {
       const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
 
       return (
-        <article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
+        <div className='status__wrapper' data-id={status.get('id')} >
           <div className='status__prepend'>
             <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
             <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
           </div>
 
-          <Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
-        </article>
+          <Status {...other} status={status.get('reblog')} account={status.get('account')} />
+        </div>
       );
     }
 
@@ -237,7 +151,7 @@ export default class Status extends ImmutablePureComponent {
     }
 
     return (
-      <article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex={wrapped ? null : '0'}  ref={this.handleRef}>
+      <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')}>
         <div className='status__info'>
           <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
 
@@ -255,7 +169,7 @@ export default class Status extends ImmutablePureComponent {
         {media}
 
         <StatusActionBar {...this.props} />
-      </article>
+      </div>
     );
   }
 
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 81c2a4e23..de99f8850 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -24,6 +24,9 @@ const messages = defineMessages({
   report: { id: 'status.report', defaultMessage: 'Report @{name}' },
   muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
   unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
+  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
+  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 });
 
 @injectIntl
@@ -43,7 +46,9 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onMute: PropTypes.func,
     onBlock: PropTypes.func,
     onReport: PropTypes.func,
+    onEmbed: PropTypes.func,
     onMuteConversation: PropTypes.func,
+    onPin: PropTypes.func,
     me: PropTypes.number,
     withDismiss: PropTypes.bool,
     intl: PropTypes.object.isRequired,
@@ -80,6 +85,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
     this.props.onDelete(this.props.status);
   }
 
+  handlePinClick = () => {
+    this.props.onPin(this.props.status);
+  }
+
   handleMentionClick = () => {
     this.props.onMention(this.props.status.get('account'), this.context.router.history);
   }
@@ -96,6 +105,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
     this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
   }
 
+  handleEmbed = () => {
+    this.props.onEmbed(this.props.status);
+  }
+
   handleReport = () => {
     this.props.onReport(this.props.status);
   }
@@ -106,9 +119,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
 
   render () {
     const { status, me, intl, withDismiss } = this.props;
-    const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
+
     const mutingConversation = status.get('muted');
-    const anonymousAccess = !me;
+    const anonymousAccess    = !me;
+    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
 
     let menu = [];
     let reblogIcon = 'retweet';
@@ -116,6 +130,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
     let replyTitle;
 
     menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
+
+    if (publicStatus) {
+      menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
+    }
+
     menu.push(null);
 
     if (withDismiss) {
@@ -124,6 +143,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
     }
 
     if (status.getIn(['account', 'id']) === me) {
+      if (publicStatus) {
+        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+      }
+
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
@@ -154,7 +177,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     return (
       <div className='status__action-bar'>
         <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
+        <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
         <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
         {shareButton}
 
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index 271cf33b7..6bd357754 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -1,12 +1,10 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { ScrollContainer } from 'react-router-scroll';
 import PropTypes from 'prop-types';
 import StatusContainer from '../../glitch/components/status/container';
 import LoadMore from './load_more';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
-import { throttle } from 'lodash';
+import ScrollableList from './scrollable_list';
 
 export default class StatusList extends ImmutablePureComponent {
 
@@ -28,145 +26,21 @@ export default class StatusList extends ImmutablePureComponent {
     trackScroll: true,
   };
 
-  intersectionObserverWrapper = new IntersectionObserverWrapper();
-
-  handleScroll = throttle(() => {
-    if (this.node) {
-      const { scrollTop, scrollHeight, clientHeight } = this.node;
-      const offset = scrollHeight - scrollTop - clientHeight;
-      this._oldScrollPosition = scrollHeight - scrollTop;
-
-      if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
-        this.props.onScrollToBottom();
-      } else if (scrollTop < 100 && this.props.onScrollToTop) {
-        this.props.onScrollToTop();
-      } else if (this.props.onScroll) {
-        this.props.onScroll();
-      }
-    }
-  }, 150, {
-    trailing: true,
-  });
-
-  componentDidMount () {
-    this.attachScrollListener();
-    this.attachIntersectionObserver();
-
-    // Handle initial scroll posiiton
-    this.handleScroll();
-  }
-
-  componentDidUpdate (prevProps) {
-    // Reset the scroll position when a new toot comes in in order not to
-    // jerk the scrollbar around if you're already scrolled down the page.
-    if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
-      if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
-        let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
-        if (this.node.scrollTop !== newScrollTop) {
-          this.node.scrollTop = newScrollTop;
-        }
-      } else {
-        this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
-      }
-    }
-  }
-
-  componentWillUnmount () {
-    this.detachScrollListener();
-    this.detachIntersectionObserver();
-  }
-
-  attachIntersectionObserver () {
-    this.intersectionObserverWrapper.connect({
-      root: this.node,
-      rootMargin: '300% 0px',
-    });
-  }
-
-  detachIntersectionObserver () {
-    this.intersectionObserverWrapper.disconnect();
-  }
-
-  attachScrollListener () {
-    this.node.addEventListener('scroll', this.handleScroll);
-  }
-
-  detachScrollListener () {
-    this.node.removeEventListener('scroll', this.handleScroll);
-  }
-
-  setRef = (c) => {
-    this.node = c;
-  }
-
-  handleLoadMore = (e) => {
-    e.preventDefault();
-    this.props.onScrollToBottom();
-  }
-
-  handleKeyDown = (e) => {
-    if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
-      const article = (() => {
-        switch (e.key) {
-        case 'PageDown':
-          return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
-        case 'PageUp':
-          return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
-        case 'End':
-          return this.node.querySelector('[role="feed"] > article:last-of-type');
-        case 'Home':
-          return this.node.querySelector('[role="feed"] > article:first-of-type');
-        default:
-          return null;
-        }
-      })();
-
-
-      if (article) {
-        e.preventDefault();
-        article.focus();
-        article.scrollIntoView();
-      }
-    }
-  }
-
   render () {
-    const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
-
-    const loadMore     = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />;
-    let scrollableArea = null;
-
-    if (isLoading || statusIds.size > 0 || !emptyMessage) {
-      scrollableArea = (
-        <div className='scrollable' ref={this.setRef}>
-          <div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
-            {prepend}
-
-            {statusIds.map((statusId, index) => {
-              return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
-            })}
-
-            {loadMore}
-          </div>
-        </div>
-      );
-    } else {
-      scrollableArea = (
-        <div className='empty-column-indicator' ref={this.setRef}>
-          {emptyMessage}
-        </div>
-      );
-    }
-
-    if (trackScroll) {
-      return (
-        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
-          {scrollableArea}
-        </ScrollContainer>
-      );
-    } else {
-      return scrollableArea;
-    }
+    const { statusIds, ...other } = this.props;
+    const { isLoading } = other;
+
+    const scrollableContent = (isLoading || statusIds.size > 0) ? (
+      statusIds.map((statusId) => (
+        <StatusContainer key={statusId} id={statusId} />
+      ))
+    ) : null;
+
+    return (
+      <ScrollableList {...other}>
+        {scrollableContent}
+      </ScrollableList>
+    );
   }
 
 }
diff --git a/app/javascript/mastodon/containers/account_container.js b/app/javascript/mastodon/containers/account_container.js
index ca1efd0e5..7c77cb764 100644
--- a/app/javascript/mastodon/containers/account_container.js
+++ b/app/javascript/mastodon/containers/account_container.js
@@ -32,7 +32,7 @@ const makeMapStateToProps = () => {
 const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onFollow (account) {
-    if (account.getIn(['relationship', 'following'])) {
+    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
       if (this.unfollowModal) {
         dispatch(openModal('CONFIRM', {
           message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js
new file mode 100644
index 000000000..db452d03a
--- /dev/null
+++ b/app/javascript/mastodon/containers/compose_container.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import PropTypes from 'prop-types';
+import configureStore from '../store/configureStore';
+import { hydrateStore } from '../actions/store';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import Compose from '../features/standalone/compose';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+const store = configureStore();
+const initialStateContainer = document.getElementById('initial-state');
+
+if (initialStateContainer !== null) {
+  const initialState = JSON.parse(initialStateContainer.textContent);
+  store.dispatch(hydrateStore(initialState));
+}
+
+export default class TimelineContainer extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+  };
+
+  render () {
+    const { locale } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        <Provider store={store}>
+          <Compose />
+        </Provider>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 8287375c4..db2a5f269 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -2,21 +2,13 @@ import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
 import configureStore from '../store/configureStore';
-import {
-  updateTimeline,
-  deleteFromTimelines,
-  refreshHomeTimeline,
-  connectTimeline,
-  disconnectTimeline,
-} from '../actions/timelines';
 import { showOnboardingOnce } from '../actions/onboarding';
-import { updateNotifications, refreshNotifications } from '../actions/notifications';
 import BrowserRouter from 'react-router-dom/BrowserRouter';
 import Route from 'react-router-dom/Route';
 import ScrollContext from 'react-router-scroll/lib/ScrollBehaviorContext';
 import UI from '../features/ui';
 import { hydrateStore } from '../actions/store';
-import createStream from '../stream';
+import { connectUserStream } from '../actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 const { localeData, messages } = getLocale();
@@ -39,74 +31,28 @@ export default class Mastodon extends React.PureComponent {
   };
 
   componentDidMount() {
-    const { locale }  = this.props;
-    const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
-    const accessToken = store.getState().getIn(['meta', 'access_token']);
-
-    const setupPolling = () => {
-      this.polling = setInterval(() => {
-        store.dispatch(refreshHomeTimeline());
-        store.dispatch(refreshNotifications());
-      }, 20000);
-    };
-
-    const clearPolling = () => {
-      clearInterval(this.polling);
-      this.polling = undefined;
-    };
-
-    this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
-
-      connected () {
-        clearPolling();
-        store.dispatch(connectTimeline('home'));
-      },
-
-      disconnected () {
-        setupPolling();
-        store.dispatch(disconnectTimeline('home'));
-      },
-
-      received (data) {
-        switch(data.event) {
-        case 'update':
-          store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
-          break;
-        case 'delete':
-          store.dispatch(deleteFromTimelines(data.payload));
-          break;
-        case 'notification':
-          store.dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
-          break;
-        }
-      },
-
-      reconnected () {
-        clearPolling();
-        store.dispatch(connectTimeline('home'));
-        store.dispatch(refreshHomeTimeline());
-        store.dispatch(refreshNotifications());
-      },
-
-    });
+    this.disconnect = store.dispatch(connectUserStream());
 
     // Desktop notifications
+    // Ask after 1 minute
     if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
-      Notification.requestPermission();
+      window.setTimeout(() => Notification.requestPermission(), 60 * 1000);
+    }
+
+    // Protocol handler
+    // Ask after 5 minutes
+    if (typeof navigator.registerProtocolHandler !== 'undefined') {
+      const handlerUrl = window.location.protocol + '//' + window.location.host + '/intent?uri=%s';
+      window.setTimeout(() => navigator.registerProtocolHandler('web+mastodon', handlerUrl, 'Mastodon'), 5 * 60 * 1000);
     }
 
     store.dispatch(showOnboardingOnce());
   }
 
   componentWillUnmount () {
-    if (typeof this.subscription !== 'undefined') {
-      this.subscription.close();
-      this.subscription = null;
-    }
-
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index d71584267..9dff79b72 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -14,6 +14,8 @@ import {
   favourite,
   unreblog,
   unfavourite,
+  pin,
+  unpin,
 } from '../actions/interactions';
 import {
   blockAccount,
@@ -75,6 +77,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onPin (status) {
+    if (status.get('pinned')) {
+      dispatch(unpin(status));
+    } else {
+      dispatch(pin(status));
+    }
+  },
+
+  onEmbed (status) {
+    dispatch(openModal('EMBED', { url: status.get('url') }));
+  },
+
   onDelete (status) {
     if (!this.deleteModal) {
       dispatch(deleteStatus(status.get('id')));
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 320e669a2..7ab492225 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -14,7 +14,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
-  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
+  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
 });
 
 const makeMapStateToProps = () => {
@@ -105,7 +105,7 @@ export default class Header extends ImmutablePureComponent {
       if (account.getIn(['relationship', 'requested'])) {
         actionBtn = (
           <div className='account--action-button'>
-            <IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
+            <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />
           </div>
         );
       } else if (!account.getIn(['relationship', 'blocking'])) {
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index baa81bbc2..dcee78b3e 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
 const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onFollow (account) {
-    if (account.getIn(['relationship', 'following'])) {
+    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
       if (this.unfollowModal) {
         dispatch(openModal('CONFIRM', {
           message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
index 0e2300f8c..596a89412 100644
--- a/app/javascript/mastodon/features/community_timeline/index.js
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
 import {
   refreshCommunityTimeline,
   expandCommunityTimeline,
-  updateTimeline,
-  deleteFromTimelines,
-  connectTimeline,
-  disconnectTimeline,
 } from '../../actions/timelines';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import createStream from '../../stream';
+import { connectCommunityStream } from '../../actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.community', defaultMessage: 'Local timeline' },
@@ -23,8 +19,6 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
-  streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
-  accessToken: state.getIn(['meta', 'access_token']),
 });
 
 @connect(mapStateToProps)
@@ -35,8 +29,6 @@ export default class CommunityTimeline extends React.PureComponent {
     dispatch: PropTypes.func.isRequired,
     columnId: PropTypes.string,
     intl: PropTypes.object.isRequired,
-    streamingAPIBaseURL: PropTypes.string.isRequired,
-    accessToken: PropTypes.string.isRequired,
     hasUnread: PropTypes.bool,
     multiColumn: PropTypes.bool,
   };
@@ -61,46 +53,16 @@ export default class CommunityTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
+    const { dispatch } = this.props;
 
     dispatch(refreshCommunityTimeline());
-
-    if (typeof this._subscription !== 'undefined') {
-      return;
-    }
-
-    this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
-
-      connected () {
-        dispatch(connectTimeline('community'));
-      },
-
-      reconnected () {
-        dispatch(connectTimeline('community'));
-      },
-
-      disconnected () {
-        dispatch(disconnectTimeline('community'));
-      },
-
-      received (data) {
-        switch(data.event) {
-        case 'update':
-          dispatch(updateTimeline('community', JSON.parse(data.payload)));
-          break;
-        case 'delete':
-          dispatch(deleteFromTimelines(data.payload));
-          break;
-        }
-      },
-
-    });
+    this.disconnect = dispatch(connectCommunityStream());
   }
 
   componentWillUnmount () {
-    if (typeof this._subscription !== 'undefined') {
-      this._subscription.close();
-      this._subscription = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index d9ad9bc1f..82b16b369 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -16,6 +16,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   statusIds: state.getIn(['status_lists', 'favourites', 'items']),
+  hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
 });
 
 @connect(mapStateToProps)
@@ -28,6 +29,7 @@ export default class Favourites extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     columnId: PropTypes.string,
     multiColumn: PropTypes.bool,
+    hasMore: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -62,7 +64,7 @@ export default class Favourites extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, statusIds, columnId, multiColumn } = this.props;
+    const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
     const pinned = !!columnId;
 
     return (
@@ -81,6 +83,7 @@ export default class Favourites extends ImmutablePureComponent {
           trackScroll={!pinned}
           statusIds={statusIds}
           scrollKey={`favourited_statuses-${columnId}`}
+          hasMore={hasMore}
           onScrollToBottom={this.handleScrollToBottom}
         />
       </Column>
diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js
index b17e8e1a5..5fe21ce90 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.js
@@ -7,17 +7,13 @@ import ColumnHeader from '../../components/column_header';
 import {
   refreshHashtagTimeline,
   expandHashtagTimeline,
-  updateTimeline,
-  deleteFromTimelines,
 } from '../../actions/timelines';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { FormattedMessage } from 'react-intl';
-import createStream from '../../stream';
+import { connectHashtagStream } from '../../actions/streaming';
 
-const mapStateToProps = state => ({
-  hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
-  streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
-  accessToken: state.getIn(['meta', 'access_token']),
+const mapStateToProps = (state, props) => ({
+  hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
 });
 
 @connect(mapStateToProps)
@@ -27,8 +23,6 @@ export default class HashtagTimeline extends React.PureComponent {
     params: PropTypes.object.isRequired,
     columnId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
-    streamingAPIBaseURL: PropTypes.string.isRequired,
-    accessToken: PropTypes.string.isRequired,
     hasUnread: PropTypes.bool,
     multiColumn: PropTypes.bool,
   };
@@ -53,28 +47,13 @@ export default class HashtagTimeline extends React.PureComponent {
   }
 
   _subscribe (dispatch, id) {
-    const { streamingAPIBaseURL, accessToken } = this.props;
-
-    this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
-
-      received (data) {
-        switch(data.event) {
-        case 'update':
-          dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
-          break;
-        case 'delete':
-          dispatch(deleteFromTimelines(data.payload));
-          break;
-        }
-      },
-
-    });
+    this.disconnect = dispatch(connectHashtagStream(id));
   }
 
   _unsubscribe () {
-    if (typeof this.subscription !== 'undefined') {
-      this.subscription.close();
-      this.subscription = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index 7d521e4b6..b52c3c934 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -2,6 +2,7 @@
 //  SEE INSTEAD : glitch/components/notification
 
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import StatusContainer from '../../../containers/status_container';
 import AccountContainer from '../../../containers/account_container';
@@ -13,6 +14,7 @@ export default class Notification extends ImmutablePureComponent {
 
   static propTypes = {
     notification: ImmutablePropTypes.map.isRequired,
+    hidden: PropTypes.bool,
   };
 
   renderFollow (account, link) {
@@ -26,13 +28,13 @@ export default class Notification extends ImmutablePureComponent {
           <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
         </div>
 
-        <AccountContainer id={account.get('id')} withNote={false} />
+        <AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
       </div>
     );
   }
 
   renderMention (notification) {
-    return <StatusContainer id={notification.get('status')} withDismiss />;
+    return <StatusContainer id={notification.get('status')} withDismiss hidden={this.props.hidden} />;
   }
 
   renderFavourite (notification, link) {
@@ -45,7 +47,7 @@ export default class Notification extends ImmutablePureComponent {
           <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
         </div>
 
-        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss />
+        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
       </div>
     );
   }
@@ -60,7 +62,7 @@ export default class Notification extends ImmutablePureComponent {
           <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
         </div>
 
-        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss />
+        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 0d86d41ce..b644718e3 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -16,8 +16,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
 import { List as ImmutableList } from 'immutable';
-import LoadMore from '../../components/load_more';
 import { debounce } from 'lodash';
+import ScrollableList from '../../components/scrollable_list';
 
 const messages = defineMessages({
   title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@@ -68,40 +68,18 @@ export default class Notifications extends React.PureComponent {
     trackScroll: true,
   };
 
-  dispatchExpandNotifications = debounce(() => {
+  handleScrollToBottom = debounce(() => {
+    this.props.dispatch(scrollTopNotifications(false));
     this.props.dispatch(expandNotifications());
   }, 300, { leading: true });
 
-  dispatchScrollToTop = debounce((top) => {
-    this.props.dispatch(scrollTopNotifications(top));
+  handleScrollToTop = debounce(() => {
+    this.props.dispatch(scrollTopNotifications(true));
   }, 100);
 
-  handleScroll = (e) => {
-    const { scrollTop, scrollHeight, clientHeight } = e.target;
-    const offset = scrollHeight - scrollTop - clientHeight;
-    this._oldScrollPosition = scrollHeight - scrollTop;
-
-    if (250 > offset && this.props.hasMore && !this.props.isLoading) {
-      this.dispatchExpandNotifications();
-    }
-
-    if (scrollTop < 100) {
-      this.dispatchScrollToTop(true);
-    } else {
-      this.dispatchScrollToTop(false);
-    }
-  }
-
-  componentDidUpdate (prevProps) {
-    if (this.node.scrollTop > 0 && (prevProps.notifications.size < this.props.notifications.size && prevProps.notifications.first() !== this.props.notifications.first() && !!this._oldScrollPosition)) {
-      this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
-    }
-  }
-
-  handleLoadMore = (e) => {
-    e.preventDefault();
-    this.dispatchExpandNotifications();
-  }
+  handleScroll = debounce(() => {
+    this.props.dispatch(scrollTopNotifications(false));
+  }, 100);
 
   handlePin = () => {
     const { columnId, dispatch } = this.props;
@@ -122,10 +100,6 @@ export default class Notifications extends React.PureComponent {
     this.column.scrollTop();
   }
 
-  setRef = (c) => {
-    this.node = c;
-  }
-
   setColumnRef = c => {
     this.column = c;
   }
@@ -133,52 +107,34 @@ export default class Notifications extends React.PureComponent {
   render () {
     const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
     const pinned = !!columnId;
+    const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
 
-    let loadMore       = '';
-    let scrollableArea = '';
-    let unread         = '';
-    let scrollContainer = '';
-
-    if (!isLoading && hasMore) {
-      loadMore = <LoadMore onClick={this.handleLoadMore} />;
-    }
-
-    if (isUnread) {
-      unread = <div className='notifications__unread-indicator' />;
-    }
+    let scrollableContent = null;
 
-    if (isLoading && this.scrollableArea) {
-      scrollableArea = this.scrollableArea;
+    if (isLoading && this.scrollableContent) {
+      scrollableContent = this.scrollableContent;
     } else if (notifications.size > 0 || hasMore) {
-      scrollableArea = (
-        <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
-          {unread}
-
-          <div>
-            {notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)}
-            {loadMore}
-          </div>
-        </div>
-      );
+      scrollableContent = notifications.map((item) => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />);
     } else {
-      scrollableArea = (
-        <div className='empty-column-indicator' ref={this.setRef}>
-          <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
-        </div>
-      );
+      scrollableContent = null;
     }
 
-    if (pinned) {
-      scrollContainer = scrollableArea;
-    } else {
-      scrollContainer = (
-        <ScrollContainer scrollKey={`notifications-${columnId}`} shouldUpdateScroll={shouldUpdateScroll}>
-          {scrollableArea}
-        </ScrollContainer>
-      );
-    }
-
-    this.scrollableArea = scrollableArea;
+    this.scrollableContent = scrollableContent;
+
+    const scrollContainer = (
+      <ScrollableList
+        scrollKey={`notifications-${columnId}`}
+        isLoading={isLoading}
+        hasMore={hasMore}
+        emptyMessage={emptyMessage}
+        onScrollToBottom={this.handleScrollToBottom}
+        onScrollToTop={this.handleScrollToTop}
+        onScroll={this.handleScroll}
+        shouldUpdateScroll={shouldUpdateScroll}
+      >
+        {scrollableContent}
+      </ScrollableList>
+    );
 
     return (
       <Column
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
index c6cad02d6..193489c63 100644
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ b/app/javascript/mastodon/features/public_timeline/index.js
@@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
 import {
   refreshPublicTimeline,
   expandPublicTimeline,
-  updateTimeline,
-  deleteFromTimelines,
-  connectTimeline,
-  disconnectTimeline,
 } from '../../actions/timelines';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import createStream from '../../stream';
+import { connectPublicStream } from '../../actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.public', defaultMessage: 'Federated timeline' },
@@ -23,8 +19,6 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
-  streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
-  accessToken: state.getIn(['meta', 'access_token']),
 });
 
 @connect(mapStateToProps)
@@ -36,8 +30,6 @@ export default class PublicTimeline extends React.PureComponent {
     intl: PropTypes.object.isRequired,
     columnId: PropTypes.string,
     multiColumn: PropTypes.bool,
-    streamingAPIBaseURL: PropTypes.string.isRequired,
-    accessToken: PropTypes.string.isRequired,
     hasUnread: PropTypes.bool,
   };
 
@@ -61,46 +53,16 @@ export default class PublicTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
+    const { dispatch } = this.props;
 
     dispatch(refreshPublicTimeline());
-
-    if (typeof this._subscription !== 'undefined') {
-      return;
-    }
-
-    this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
-
-      connected () {
-        dispatch(connectTimeline('public'));
-      },
-
-      reconnected () {
-        dispatch(connectTimeline('public'));
-      },
-
-      disconnected () {
-        dispatch(disconnectTimeline('public'));
-      },
-
-      received (data) {
-        switch(data.event) {
-        case 'update':
-          dispatch(updateTimeline('public', JSON.parse(data.payload)));
-          break;
-        case 'delete':
-          dispatch(deleteFromTimelines(data.payload));
-          break;
-        }
-      },
-
-    });
+    this.disconnect = dispatch(connectPublicStream());
   }
 
   componentWillUnmount () {
-    if (typeof this._subscription !== 'undefined') {
-      this._subscription.close();
-      this._subscription = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/standalone/compose/index.js b/app/javascript/mastodon/features/standalone/compose/index.js
new file mode 100644
index 000000000..96d07fefb
--- /dev/null
+++ b/app/javascript/mastodon/features/standalone/compose/index.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import ComposeFormContainer from '../../compose/containers/compose_form_container';
+import NotificationsContainer from '../../ui/containers/notifications_container';
+import LoadingBarContainer from '../../ui/containers/loading_bar_container';
+
+export default class Compose extends React.PureComponent {
+
+  render () {
+    return (
+      <div>
+        <ComposeFormContainer />
+        <NotificationsContainer />
+        <LoadingBarContainer className='loading-bar' />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index a2885adda..4be013be7 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -14,6 +14,9 @@ const messages = defineMessages({
   favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
   report: { id: 'status.report', defaultMessage: 'Report @{name}' },
   share: { id: 'status.share', defaultMessage: 'Share' },
+  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
+  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
+  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 });
 
 @injectIntl
@@ -31,6 +34,8 @@ export default class ActionBar extends React.PureComponent {
     onDelete: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
     onReport: PropTypes.func,
+    onPin: PropTypes.func,
+    onEmbed: PropTypes.func,
     me: PropTypes.number.isRequired,
     intl: PropTypes.object.isRequired,
   };
@@ -59,6 +64,10 @@ export default class ActionBar extends React.PureComponent {
     this.props.onReport(this.props.status);
   }
 
+  handlePinClick = () => {
+    this.props.onPin(this.props.status);
+  }
+
   handleShare = () => {
     navigator.share({
       text: this.props.status.get('search_index'),
@@ -66,12 +75,26 @@ export default class ActionBar extends React.PureComponent {
     });
   }
 
+  handleEmbed = () => {
+    this.props.onEmbed(this.props.status);
+  }
+
   render () {
     const { status, me, intl } = this.props;
 
+    const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
+
     let menu = [];
 
+    if (publicStatus) {
+      menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
+    }
+
     if (me === status.getIn(['account', 'id'])) {
+      if (publicStatus) {
+        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+      }
+
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index bfb40468b..6b13e15cc 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import punycode from 'punycode';
+import classnames from 'classnames';
 
 const IDNA_PREFIX = 'xn--';
 
@@ -32,7 +33,7 @@ export default class Card extends React.PureComponent {
     if (card.get('image')) {
       image = (
         <div className='status-card__image'>
-          <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' />
+          <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} />
         </div>
       );
     }
@@ -41,8 +42,12 @@ export default class Card extends React.PureComponent {
       provider = decodeIDNA(getHostname(card.get('url')));
     }
 
+    const className = classnames('status-card', {
+      'horizontal': card.get('width') > card.get('height'),
+    });
+
     return (
-      <a href={card.get('url')} className='status-card' target='_blank' rel='noopener'>
+      <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
         {image}
 
         <div className='status-card__content'>
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index d774dfdfe..03010cf0a 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -12,6 +12,8 @@ import {
   unfavourite,
   reblog,
   unreblog,
+  pin,
+  unpin,
 } from '../../actions/interactions';
 import {
   replyCompose,
@@ -89,6 +91,14 @@ export default class Status extends ImmutablePureComponent {
     }
   }
 
+  handlePin = (status) => {
+    if (status.get('pinned')) {
+      this.props.dispatch(unpin(status));
+    } else {
+      this.props.dispatch(pin(status));
+    }
+  }
+
   handleReplyClick = (status) => {
     this.props.dispatch(replyCompose(status, this.context.router.history));
   }
@@ -139,6 +149,10 @@ export default class Status extends ImmutablePureComponent {
     this.props.dispatch(initReport(status.get('account'), status));
   }
 
+  handleEmbed = (status) => {
+    this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
+  }
+
   renderChildren (list) {
     return list.map(id => <StatusContainer key={id} id={id} />);
   }
@@ -190,6 +204,8 @@ export default class Status extends ImmutablePureComponent {
               onDelete={this.handleDeleteClick}
               onMention={this.handleMentionClick}
               onReport={this.handleReport}
+              onPin={this.handlePin}
+              onEmbed={this.handleEmbed}
             />
 
             {descendants}
diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js
index 9031c16fc..15538ea38 100644
--- a/app/javascript/mastodon/features/ui/components/column.js
+++ b/app/javascript/mastodon/features/ui/components/column.js
@@ -25,6 +25,17 @@ export default class Column extends React.PureComponent {
     this._interruptScrollAnimation = scrollTop(scrollable);
   }
 
+  scrollTop () {
+    const scrollable = this.node.querySelector('.scrollable');
+
+    if (!scrollable) {
+      return;
+    }
+
+    this._interruptScrollAnimation = scrollTop(scrollable);
+  }
+
+
   handleScroll = debounce(() => {
     if (typeof this._interruptScrollAnimation !== 'undefined') {
       this._interruptScrollAnimation();
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 47d5a2e20..7d84bece7 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -12,6 +12,7 @@ import ColumnLoading from './column_loading';
 import BundleColumnError from './bundle_column_error';
 import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
 
+import detectPassiveEvents from 'detect-passive-events';
 import { scrollRight } from '../../../scroll';
 
 const componentMap = {
@@ -24,7 +25,7 @@ const componentMap = {
   'FAVOURITES': FavouritedStatuses,
 };
 
-@injectIntl
+@component => injectIntl(component, { withRef: true })
 export default class ColumnsArea extends ImmutablePureComponent {
 
   static contextTypes = {
@@ -47,16 +48,36 @@ export default class ColumnsArea extends ImmutablePureComponent {
   }
 
   componentDidMount() {
+    if (!this.props.singleColumn) {
+      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+    }
     this.lastIndex = getIndex(this.context.router.history.location.pathname);
     this.setState({ shouldAnimate: true });
   }
 
+  componentWillUpdate(nextProps) {
+    if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
+      this.node.removeEventListener('wheel', this.handleWheel);
+    }
+  }
+
   componentDidUpdate(prevProps) {
+    if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
+      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+    }
     this.lastIndex = getIndex(this.context.router.history.location.pathname);
     this.setState({ shouldAnimate: true });
+  }
 
-    if (this.props.children !== prevProps.children && !this.props.singleColumn) {
-      scrollRight(this.node);
+  componentWillUnmount () {
+    if (!this.props.singleColumn) {
+      this.node.removeEventListener('wheel', this.handleWheel);
+    }
+  }
+
+  handleChildrenContentChange() {
+    if (!this.props.singleColumn) {
+      scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
     }
   }
 
@@ -80,6 +101,14 @@ export default class ColumnsArea extends ImmutablePureComponent {
     }
   }
 
+  handleWheel = () => {
+    if (typeof this._interruptScrollAnimation !== 'function') {
+      return;
+    }
+
+    this._interruptScrollAnimation();
+  }
+
   setRef = (node) => {
     this.node = node;
   }
diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.js b/app/javascript/mastodon/features/ui/components/embed_modal.js
new file mode 100644
index 000000000..992aed8a3
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/embed_modal.js
@@ -0,0 +1,84 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import axios from 'axios';
+
+@injectIntl
+export default class EmbedModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    url: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  }
+
+  state = {
+    loading: false,
+    oembed: null,
+  };
+
+  componentDidMount () {
+    const { url } = this.props;
+
+    this.setState({ loading: true });
+
+    axios.post('/api/web/embed', { url }).then(res => {
+      this.setState({ loading: false, oembed: res.data });
+
+      const iframeDocument = this.iframe.contentWindow.document;
+
+      iframeDocument.open();
+      iframeDocument.write(res.data.html);
+      iframeDocument.close();
+
+      iframeDocument.body.style.margin = 0;
+      this.iframe.height = iframeDocument.body.scrollHeight + 'px';
+    });
+  }
+
+  setIframeRef = c =>  {
+    this.iframe = c;
+  }
+
+  handleTextareaClick = (e) => {
+    e.target.select();
+  }
+
+  render () {
+    const { oembed } = this.state;
+
+    return (
+      <div className='modal-root__modal embed-modal'>
+        <h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4>
+
+        <div className='embed-modal__container'>
+          <p className='hint'>
+            <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
+          </p>
+
+          <input
+            type='text'
+            className='embed-modal__html'
+            readOnly
+            value={oembed && oembed.html || ''}
+            onClick={this.handleTextareaClick}
+          />
+
+          <p className='hint'>
+            <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
+          </p>
+
+          <iframe
+            className='embed-modal__iframe'
+            scrolling='no'
+            frameBorder='0'
+            ref={this.setIframeRef}
+            title='preview'
+          />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index d316ff433..cd605d7b2 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -14,6 +14,7 @@ import {
   ConfirmationModal,
   ReportModal,
   SettingsModal,
+  EmbedModal,
 } from '../../../features/ui/util/async-components';
 
 const MODAL_COMPONENTS = {
@@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
   'REPORT': ReportModal,
   'SETTINGS': SettingsModal,
   'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
+  'EMBED': EmbedModal,
 };
 
 export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
index 0c872f40d..2facf9c44 100644
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -30,7 +30,7 @@ const PageOne = ({ acct, domain }) => (
     <div>
       <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1>
       <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p>
-      <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }} /></p>
+      <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>@{acct}@{domain}</strong> }} /></p>
     </div>
   </div>
 );
diff --git a/app/javascript/mastodon/features/ui/containers/columns_area_container.js b/app/javascript/mastodon/features/ui/containers/columns_area_container.js
index 6420f0784..95f95618b 100644
--- a/app/javascript/mastodon/features/ui/containers/columns_area_container.js
+++ b/app/javascript/mastodon/features/ui/containers/columns_area_container.js
@@ -5,4 +5,4 @@ const mapStateToProps = state => ({
   columns: state.getIn(['settings', 'columns']),
 });
 
-export default connect(mapStateToProps)(ColumnsArea);
+export default connect(mapStateToProps, null, null, { withRef: true })(ColumnsArea);
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 6d53f474d..883466602 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -1,12 +1,11 @@
 import React from 'react';
-import classNames from 'classnames';
-import Redirect from 'react-router-dom/Redirect';
 import NotificationsContainer from './containers/notifications_container';
 import PropTypes from 'prop-types';
 import LoadingBarContainer from './containers/loading_bar_container';
 import TabsBar from './components/tabs_bar';
 import ModalContainer from './containers/modal_container';
 import { connect } from 'react-redux';
+import { Redirect, withRouter } from 'react-router-dom';
 import { isMobile } from '../../is_mobile';
 import { debounce } from 'lodash';
 import { uploadCompose } from '../../actions/compose';
@@ -51,6 +50,7 @@ const mapStateToProps = state => ({
 });
 
 @connect(mapStateToProps)
+@withRouter
 export default class UI extends React.PureComponent {
 
   static contextTypes = {
@@ -65,6 +65,7 @@ export default class UI extends React.PureComponent {
     systemFontUi: PropTypes.bool,
     navbarUnder: PropTypes.bool,
     isComposing: PropTypes.bool,
+    location: PropTypes.object,
   };
 
   state = {
@@ -141,7 +142,7 @@ export default class UI extends React.PureComponent {
     if (data.type === 'navigate') {
       this.context.router.history.push(data.path);
     } else {
-      console.warn('Unknown message type:', data.type); // eslint-disable-line no-console
+      console.warn('Unknown message type:', data.type);
     }
   }
 
@@ -175,6 +176,12 @@ export default class UI extends React.PureComponent {
     return true;
   }
 
+  componentDidUpdate (prevProps) {
+    if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
+      this.columnsAreaNode.handleChildrenContentChange();
+    }
+  }
+
   componentWillUnmount () {
     window.removeEventListener('resize', this.handleResize);
     document.removeEventListener('dragenter', this.handleDragEnter);
@@ -188,6 +195,10 @@ export default class UI extends React.PureComponent {
     this.node = c;
   }
 
+  setColumnsAreaRef = (c) => {
+    this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
+  }
+
   render () {
     const { width, draggingOver } = this.state;
     const { children, layout, isWide, navbarUnder } = this.props;
@@ -212,7 +223,7 @@ export default class UI extends React.PureComponent {
     return (
       <div className={className} ref={this.setRef}>
         {navbarUnder ? null : (<TabsBar />)}
-        <ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
+        <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
           <WrappedSwitch>
             <Redirect from='/' to='/getting-started' exact />
             <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 9267519dd..108ffc142 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -116,3 +116,7 @@ export function MediaGallery () {
 export function VideoPlayer () {
   return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
 }
+
+export function EmbedModal () {
+  return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
+}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index f5cf77f92..2ceb6eb9a 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -47,7 +47,7 @@
   "compose_form.lock_disclaimer.lock": "مقفل",
   "compose_form.placeholder": "فيمَ تفكّر؟",
   "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
-  "compose_form.publish": "بوّق !",
+  "compose_form.publish": "بوّق",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس",
   "compose_form.spoiler": "أخفِ النص واعرض تحذيرا",
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "الأنشطة",
   "emoji_button.flags": "الأعلام",
   "emoji_button.food": "الطعام والشراب",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
   "status.delete": "إحذف",
+  "status.embed": "Embed",
   "status.favourite": "أضف إلى المفضلة",
   "status.load_more": "حمّل المزيد",
   "status.media_hidden": "الصورة مستترة",
   "status.mention": "أذكُر @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "وسع هذه المشاركة",
+  "status.pin": "Pin on profile",
   "status.reblog": "رَقِّي",
   "status.reblogged_by": "{name} رقى",
   "status.reply": "ردّ",
@@ -179,6 +183,7 @@
   "status.show_less": "إعرض أقلّ",
   "status.show_more": "أظهر المزيد",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "تحرير",
   "tabs_bar.federated_timeline": "الموحَّد",
   "tabs_bar.home": "الرئيسية",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index e6788f9eb..183ba2673 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Изтриване",
+  "status.embed": "Embed",
   "status.favourite": "Предпочитани",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Споменаване",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Споделяне",
   "status.reblogged_by": "{name} сподели",
   "status.reply": "Отговор",
@@ -179,6 +183,7 @@
   "status.show_less": "Show less",
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Съставяне",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Начало",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 95b3c60bf..0e3d2bc18 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Estàs segur que vols silenciar {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activitat",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Menjar i Beure",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
   "status.delete": "Esborrar",
+  "status.embed": "Embed",
   "status.favourite": "Favorit",
   "status.load_more": "Carrega més",
   "status.media_hidden": "Multimèdia amagat",
   "status.mention": "Esmentar @{name}",
   "status.mute_conversation": "Silenciar conversació",
   "status.open": "Ampliar aquest estat",
+  "status.pin": "Pin on profile",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} ha retootejat",
   "status.reply": "Respondre",
@@ -179,6 +183,7 @@
   "status.show_less": "Mostra menys",
   "status.show_more": "Mostra més",
   "status.unmute_conversation": "Activar conversació",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Compondre",
   "tabs_bar.federated_timeline": "Federada",
   "tabs_bar.home": "Inici",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 67a99b765..38324e156 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -1,31 +1,31 @@
 {
   "account.block": "@{name} blocken",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.block_domain": "Alles von {domain} verstecken",
+  "account.disclaimer_full": "Hier aufgeführten Informationen können unvollständig sein.",
   "account.edit_profile": "Profil bearbeiten",
   "account.follow": "Folgen",
   "account.followers": "Folgende",
   "account.follows": "Folgt",
   "account.follows_you": "Folgt dir",
-  "account.media": "Media",
+  "account.media": "Medien",
   "account.mention": "@{name} erwähnen",
   "account.mute": "@{name} stummschalten",
   "account.posts": "Beiträge",
   "account.report": "@{name} melden",
-  "account.requested": "Warte auf Erlaubnis",
-  "account.share": "Share @{name}'s profile",
+  "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
+  "account.share": "Profil von @{name} teilen",
   "account.unblock": "@{name} entblocken",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "{domain} wieder anzeigen",
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "Komplettes Profil anzeigen",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
+  "bundle_column_error.retry": "Erneut versuchen",
+  "bundle_column_error.title": "Netzwerkfehlher",
+  "bundle_modal_error.close": "Schließen",
+  "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
+  "bundle_modal_error.retry": "Erneut versuchen",
   "column.blocks": "Blockierte Benutzer",
   "column.community": "Lokale Zeitleiste",
   "column.favourites": "Favoriten",
@@ -35,16 +35,16 @@
   "column.notifications": "Mitteilungen",
   "column.public": "Gesamtes bekanntes Netz",
   "column_back_button.label": "Zurück",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "Einstellungen verbergen",
+  "column_header.moveLeft_settings": "Spalte links verschieben",
+  "column_header.moveRight_settings": "Spalte rechts verschieben",
+  "column_header.pin": "Anheften",
+  "column_header.show_settings": "Einstellungen anzeigen",
+  "column_header.unpin": "Lösen",
   "column_subheading.navigation": "Navigation",
-  "column_subheading.settings": "Settings",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
-  "compose_form.lock_disclaimer.lock": "locked",
+  "column_subheading.settings": "Einstellungen",
+  "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.",
+  "compose_form.lock_disclaimer.lock": "gesperrt",
   "compose_form.placeholder": "Worüber möchtest du schreiben?",
   "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
   "compose_form.publish": "Tröt",
@@ -52,41 +52,43 @@
   "compose_form.sensitive": "Medien als heikel markieren",
   "compose_form.spoiler": "Text hinter Warnung verbergen",
   "compose_form.spoiler_placeholder": "Inhaltswarnung",
-  "confirmation_modal.cancel": "Cancel",
-  "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
-  "confirmations.delete.confirm": "Delete",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
-  "confirmations.mute.confirm": "Mute",
-  "confirmations.mute.message": "Are you sure you want to mute {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "emoji_button.activity": "Activity",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
+  "confirmation_modal.cancel": "Abbrechen",
+  "confirmations.block.confirm": "Blockieren",
+  "confirmations.block.message": "Bist du dir sicher, dass du {name} blockieren möchtest?",
+  "confirmations.delete.confirm": "Löschen",
+  "confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchstest?",
+  "confirmations.domain_block.confirm": "Die ganze Domain verbergen",
+  "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} verbergen willst? In den meisten Fällen sind ein paar gezielte Blocks genug.",
+  "confirmations.mute.confirm": "Stummschalten",
+  "confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchstest?",
+  "confirmations.unfollow.confirm": "Entfolgen",
+  "confirmations.unfollow.message": "Bist du dir sicher, dass du {name} entfolgen möchstest?",
+  "embed.instructions": "Du kannst diesen Beitrag auf deiner Webseite einbetten, in dem du den folgenden Code einfügst.",
+  "embed.preview": "So wird es aussehen:",
+  "emoji_button.activity": "Aktivitäten",
+  "emoji_button.flags": "Flaggen",
+  "emoji_button.food": "Essen und Trinken",
   "emoji_button.label": "Emoji einfügen",
-  "emoji_button.nature": "Nature",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.search": "Search...",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
+  "emoji_button.nature": "Natur",
+  "emoji_button.objects": "Dinge",
+  "emoji_button.people": "Leute",
+  "emoji_button.search": "Suche…",
+  "emoji_button.symbols": "Symbole",
+  "emoji_button.travel": "Reise und Orte",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",
   "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.",
   "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
+  "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.",
   "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.",
   "follow_request.authorize": "Erlauben",
   "follow_request.reject": "Ablehnen",
-  "getting_started.appsshort": "Apps",
-  "getting_started.faq": "FAQ",
+  "getting_started.appsshort": "Anwendungen",
+  "getting_started.faq": "Häufig gestellte Fragen",
   "getting_started.heading": "Erste Schritte",
   "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
-  "getting_started.userguide": "User Guide",
+  "getting_started.userguide": "Nutzeranleitung",
   "home.column_settings.advanced": "Fortgeschritten",
   "home.column_settings.basic": "Einfach",
   "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
@@ -94,8 +96,8 @@
   "home.column_settings.show_replies": "Antworten anzeigen",
   "home.settings": "Spalteneinstellungen",
   "lightbox.close": "Schließen",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "Weiter",
+  "lightbox.previous": "Zurück",
   "loading_indicator.label": "Lade…",
   "media_gallery.toggle_visible": "Sichtbarkeit einstellen",
   "missing_indicator.label": "Nicht gefunden",
@@ -113,8 +115,8 @@
   "notification.follow": "{name} folgt dir",
   "notification.mention": "{name} erwähnte dich",
   "notification.reblog": "{name} teilte deinen Status",
-  "notifications.clear": "Mitteilungen beseitigen",
-  "notifications.clear_confirmation": "Bist du sicher, dass du alle Mitteilungen beseitigen willst?",
+  "notifications.clear": "Mitteilungen löschen",
+  "notifications.clear_confirmation": "Bist du dir sicher, dass du alle Mitteilungen löschen möchstest?",
   "notifications.column_settings.alert": "Desktop-Benachrichtigungen",
   "notifications.column_settings.favourite": "Favorisierungen:",
   "notifications.column_settings.follow": "Neue Folgende:",
@@ -124,26 +126,26 @@
   "notifications.column_settings.reblog": "Geteilte Beiträge:",
   "notifications.column_settings.show": "In der Spalte anzeigen",
   "notifications.column_settings.sound": "Ton abspielen",
-  "onboarding.done": "Done",
-  "onboarding.next": "Next",
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
-  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
-  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
-  "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
-  "onboarding.page_one.welcome": "Welcome to Mastodon!",
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
-  "onboarding.page_six.almost_done": "Almost done...",
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
-  "onboarding.page_six.guidelines": "community guidelines",
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
-  "onboarding.page_six.various_app": "mobile apps",
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
-  "onboarding.skip": "Skip",
+  "onboarding.done": "Fertig",
+  "onboarding.next": "Weiter",
+  "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf deiner Instanz {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, die kollektiv aus deiner Instanz heraus gefolgt werden. Zusammen werden die beiden Leisten auch öffentliche Zeitleisten genannt, durch sie kannst du viel neues entdecken.",
+  "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
+  "onboarding.page_four.notifications": "Wenn jemand mir dir interagiert, bekommst du eine Mitteilung.",
+  "onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.",
+  "onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Nutzername im Netzwerk {handle}",
+  "onboarding.page_one.welcome": "Willkommen bei Mastodon!",
+  "onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.",
+  "onboarding.page_six.almost_done": "Fast fertig…",
+  "onboarding.page_six.appetoot": "Guten Appetröt!",
+  "onboarding.page_six.apps_available": "Es gibt verschiedene {apps} für iOS, Android und andere Plattformen.",
+  "onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
+  "onboarding.page_six.guidelines": "Richtlinien",
+  "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
+  "onboarding.page_six.various_app": "mobile Anwendungen",
+  "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.",
+  "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Nutzernamen.",
+  "onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.",
+  "onboarding.skip": "Überspringen",
   "privacy.change": "Privatsphäre des Status anpassen",
   "privacy.direct.long": "Beitrag nur an erwähnte Benutzer",
   "privacy.direct.short": "Direkt",
@@ -159,15 +161,17 @@
   "report.target": "Melden",
   "search.placeholder": "Suche",
   "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
-  "standalone.public_title": "A look inside...",
-  "status.cannot_reblog": "This post cannot be boosted",
+  "standalone.public_title": "Vorschau…",
+  "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
   "status.delete": "Löschen",
+  "status.embed": "Einbetten",
   "status.favourite": "Favorisieren",
   "status.load_more": "Weitere laden",
   "status.media_hidden": "Medien versteckt",
   "status.mention": "Erwähnen",
-  "status.mute_conversation": "Mute conversation",
+  "status.mute_conversation": "Thread stummschalten",
   "status.open": "Öffnen",
+  "status.pin": "Auf dem Profil anheften",
   "status.reblog": "Teilen",
   "status.reblogged_by": "{name} teilte",
   "status.reply": "Antworten",
@@ -175,13 +179,14 @@
   "status.report": "@{name} melden",
   "status.sensitive_toggle": "Klicke, um sie zu sehen",
   "status.sensitive_warning": "Heikle Inhalte",
-  "status.share": "Share",
+  "status.share": "Teilen",
   "status.show_less": "Weniger anzeigen",
   "status.show_more": "Mehr anzeigen",
-  "status.unmute_conversation": "Unmute conversation",
+  "status.unmute_conversation": "Stummschaltung von Thread aufheben",
+  "status.unpin": "Vom Profil lösen",
   "tabs_bar.compose": "Schreiben",
   "tabs_bar.federated_timeline": "Föderation",
-  "tabs_bar.home": "Home",
+  "tabs_bar.home": "Startseite",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Mitteilungen",
   "upload_area.title": "Hereinziehen zum Hochladen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index e5d541cd6..89f74a56b 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -189,6 +189,18 @@
       {
         "defaultMessage": "Unmute conversation",
         "id": "status.unmute_conversation"
+      },
+      {
+        "defaultMessage": "Pin on profile",
+        "id": "status.pin"
+      },
+      {
+        "defaultMessage": "Unpin from profile",
+        "id": "status.unpin"
+      },
+      {
+        "defaultMessage": "Embed",
+        "id": "status.embed"
       }
     ],
     "path": "app/javascript/mastodon/components/status_action_bar.json"
@@ -424,7 +436,7 @@
         "id": "account.follow"
       },
       {
-        "defaultMessage": "Awaiting approval",
+        "defaultMessage": "Awaiting approval. Click to cancel follow request",
         "id": "account.requested"
       },
       {
@@ -1035,6 +1047,18 @@
       {
         "defaultMessage": "Share",
         "id": "status.share"
+      },
+      {
+        "defaultMessage": "Pin on profile",
+        "id": "status.pin"
+      },
+      {
+        "defaultMessage": "Unpin from profile",
+        "id": "status.unpin"
+      },
+      {
+        "defaultMessage": "Embed",
+        "id": "status.embed"
       }
     ],
     "path": "app/javascript/mastodon/features/status/components/action_bar.json"
@@ -1111,6 +1135,23 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Embed",
+        "id": "status.embed"
+      },
+      {
+        "defaultMessage": "Embed this status on your website by copying the code below.",
+        "id": "embed.instructions"
+      },
+      {
+        "defaultMessage": "Here is what it will look like:",
+        "id": "embed.preview"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/embed_modal.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Close",
         "id": "lightbox.close"
       },
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 2ea2062d3..6d9b9c208 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -12,7 +12,7 @@
   "account.mute": "Mute @{name}",
   "account.posts": "Posts",
   "account.report": "Report @{name}",
-  "account.requested": "Awaiting approval",
+  "account.requested": "Awaiting approval. Click to cancel follow request",
   "account.share": "Share @{name}'s profile",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unhide {domain}",
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Delete",
+  "status.embed": "Embed",
   "status.favourite": "Favourite",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mention @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boosted",
   "status.reply": "Reply",
@@ -179,6 +183,7 @@
   "status.show_less": "Show less",
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Compose",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Home",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 960d747ec..d828d0858 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Forigi",
+  "status.embed": "Embed",
   "status.favourite": "Favori",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mencii @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Diskonigi",
   "status.reblogged_by": "{name} diskonigita",
   "status.reply": "Respondi",
@@ -179,6 +183,7 @@
   "status.show_less": "Show less",
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Ekskribi",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Hejmo",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 212d16639..d35eb84e7 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Borrar",
+  "status.embed": "Embed",
   "status.favourite": "Favorito",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mencionar",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expandir estado",
+  "status.pin": "Pin on profile",
   "status.reblog": "Retoot",
   "status.reblogged_by": "Retooteado por {name}",
   "status.reply": "Responder",
@@ -179,6 +183,7 @@
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar más",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Redactar",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Inicio",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 5ada62f93..b51340fa7 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
   "confirmations.unfollow.confirm": "لغو پیگیری",
   "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "فعالیت",
   "emoji_button.flags": "پرچم‌ها",
   "emoji_button.food": "غذا و نوشیدنی",
@@ -162,12 +164,14 @@
   "standalone.public_title": "نگاهی به کاربران این سرور...",
   "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
   "status.delete": "پاک‌کردن",
+  "status.embed": "Embed",
   "status.favourite": "پسندیدن",
   "status.load_more": "بیشتر نشان بده",
   "status.media_hidden": "تصویر پنهان شده",
   "status.mention": "نام‌بردن از @{name}",
   "status.mute_conversation": "بی‌صداکردن گفتگو",
   "status.open": "این نوشته را باز کن",
+  "status.pin": "Pin on profile",
   "status.reblog": "بازبوقیدن",
   "status.reblogged_by": "‫{name}‬ بازبوقید",
   "status.reply": "پاسخ",
@@ -179,6 +183,7 @@
   "status.show_less": "نهفتن",
   "status.show_more": "نمایش",
   "status.unmute_conversation": "باصداکردن گفتگو",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "بنویسید",
   "tabs_bar.federated_timeline": "همگانی",
   "tabs_bar.home": "خانه",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index cb9e9c2a6..926a57ff1 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Poista",
+  "status.embed": "Embed",
   "status.favourite": "Tykkää",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mainitse @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Buustaa",
   "status.reblogged_by": "{name} buustasi",
   "status.reply": "Vastaa",
@@ -179,6 +183,7 @@
   "status.show_less": "Show less",
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Luo",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Koti",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index f3f0d0463..fa8ea6c73 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -20,11 +20,11 @@
   "account.unmute": "Ne plus masquer",
   "account.view_full_profile": "Afficher le profil complet",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
-  "bundle_column_error.body": "Une erreur s'est produite lors du chargement de ce composant.",
+  "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
   "bundle_column_error.retry": "Réessayer",
   "bundle_column_error.title": "Erreur réseau",
   "bundle_modal_error.close": "Fermer",
-  "bundle_modal_error.message": "Une erreur s'est produite lors du chargement de ce composant.",
+  "bundle_modal_error.message": "Une erreur s’est produite lors du chargement de ce composant.",
   "bundle_modal_error.retry": "Réessayer",
   "column.blocks": "Comptes bloqués",
   "column.community": "Fil public local",
@@ -43,12 +43,12 @@
   "column_header.unpin": "Retirer",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Paramètres",
-  "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
+  "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
   "compose_form.placeholder": "Qu’avez-vous en tête ?",
   "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
   "compose_form.publish": "Pouet ",
-  "compose_form.publish_loud": "{publish}!",
+  "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive": "Marquer le média comme sensible",
   "compose_form.spoiler": "Masquer le texte derrière un avertissement",
   "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
@@ -62,7 +62,9 @@
   "confirmations.mute.confirm": "Masquer",
   "confirmations.mute.message": "Confirmez vous le masquage de {name} ?",
   "confirmations.unfollow.confirm": "Ne plus suivre",
-  "confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name} ?",
+  "confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name} ?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activités",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
@@ -134,8 +136,8 @@
   "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
   "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}",
   "onboarding.page_six.almost_done": "Nous y sommes presque…",
-  "onboarding.page_six.appetoot": "Bon Appétoot!",
-  "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appétoot!",
+  "onboarding.page_six.appetoot": "Bon appouétit !",
+  "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon appouétit !",
   "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
   "onboarding.page_six.guidelines": "règles de la communauté",
   "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
@@ -159,15 +161,17 @@
   "report.target": "Signalement",
   "search.placeholder": "Rechercher",
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
-  "standalone.public_title": "Coup d'œil",
+  "standalone.public_title": "Jeter un coup d’œil…",
   "status.cannot_reblog": "Cette publication ne peut être boostée",
   "status.delete": "Effacer",
+  "status.embed": "Embed",
   "status.favourite": "Ajouter aux favoris",
   "status.load_more": "Charger plus",
   "status.media_hidden": "Média caché",
   "status.mention": "Mentionner",
   "status.mute_conversation": "Masquer la conversation",
   "status.open": "Déplier ce statut",
+  "status.pin": "Épingler sur le profil",
   "status.reblog": "Partager",
   "status.reblogged_by": "{name} a partagé :",
   "status.reply": "Répondre",
@@ -179,6 +183,7 @@
   "status.show_less": "Replier",
   "status.show_more": "Déplier",
   "status.unmute_conversation": "Ne plus masquer la conversation",
+  "status.unpin": "Retirer du profil",
   "tabs_bar.compose": "Composer",
   "tabs_bar.federated_timeline": "Fil public global",
   "tabs_bar.home": "Accueil",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 34266d8e1..9ef933108 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "להשתיק את {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "פעילות",
   "emoji_button.flags": "דגלים",
   "emoji_button.food": "אוכל ושתיה",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "לא ניתן להדהד הודעה זו",
   "status.delete": "מחיקה",
+  "status.embed": "Embed",
   "status.favourite": "חיבוב",
   "status.load_more": "עוד",
   "status.media_hidden": "מדיה מוסתרת",
   "status.mention": "פניה אל @{name}",
   "status.mute_conversation": "השתקת שיחה",
   "status.open": "הרחבת הודעה",
+  "status.pin": "Pin on profile",
   "status.reblog": "הדהוד",
   "status.reblogged_by": "הודהד על ידי {name}",
   "status.reply": "תגובה",
@@ -179,6 +183,7 @@
   "status.show_less": "הראה פחות",
   "status.show_more": "הראה יותר",
   "status.unmute_conversation": "הסרת השתקת שיחה",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "חיבור",
   "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
   "tabs_bar.home": "בבית",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index f69b096d4..27e943bdd 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivnost",
   "emoji_button.flags": "Zastave",
   "emoji_button.food": "Hrana & Piće",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "Ovaj post ne može biti podignut",
   "status.delete": "Obriši",
+  "status.embed": "Embed",
   "status.favourite": "Označi omiljenim",
   "status.load_more": "Učitaj više",
   "status.media_hidden": "Sakriven media sadržaj",
   "status.mention": "Spomeni @{name}",
   "status.mute_conversation": "Utišaj razgovor",
   "status.open": "Proširi ovaj status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Podigni",
   "status.reblogged_by": "{name} je podigao",
   "status.reply": "Odgovori",
@@ -179,6 +183,7 @@
   "status.show_less": "Pokaži manje",
   "status.show_more": "Pokaži više",
   "status.unmute_conversation": "Poništi utišavanje razgovora",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Sastavi",
   "tabs_bar.federated_timeline": "Federalni",
   "tabs_bar.home": "Dom",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 4d2a50963..a708ec638 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Törlés",
+  "status.embed": "Embed",
   "status.favourite": "Kedvenc",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Említés",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Reblog",
   "status.reblogged_by": "{name} reblogolta",
   "status.reply": "Válasz",
@@ -179,6 +183,7 @@
   "status.show_less": "Show less",
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Összeállítás",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Kezdőlap",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 532739e3c..d71e293e8 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivitas",
   "emoji_button.flags": "Bendera",
   "emoji_button.food": "Makanan & Minuman",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Hapus",
+  "status.embed": "Embed",
   "status.favourite": "Difavoritkan",
   "status.load_more": "Tampilkan semua",
   "status.media_hidden": "Media disembunyikan",
   "status.mention": "Balasan @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Tampilkan status ini",
+  "status.pin": "Pin on profile",
   "status.reblog": "Boost",
   "status.reblogged_by": "di-boost {name}",
   "status.reply": "Balas",
@@ -179,6 +183,7 @@
   "status.show_less": "Tampilkan lebih sedikit",
   "status.show_more": "Tampilkan semua",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Tulis",
   "tabs_bar.federated_timeline": "Gabungan",
   "tabs_bar.home": "Beranda",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index a5e363e40..5df5c59a1 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Efacar",
+  "status.embed": "Embed",
   "status.favourite": "Favorizar",
   "status.load_more": "Kargar pluse",
   "status.media_hidden": "Kontenajo celita",
   "status.mention": "Mencionar @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Detaligar ca mesajo",
+  "status.pin": "Pin on profile",
   "status.reblog": "Repetar",
   "status.reblogged_by": "{name} repetita",
   "status.reply": "Respondar",
@@ -179,6 +183,7 @@
   "status.show_less": "Montrar mine",
   "status.show_more": "Montrar plue",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Kompozar",
   "tabs_bar.federated_timeline": "Federata",
   "tabs_bar.home": "Hemo",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 329eb82ca..eec35a70c 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Elimina",
+  "status.embed": "Embed",
   "status.favourite": "Apprezzato",
   "status.load_more": "Mostra di più",
   "status.media_hidden": "Allegato nascosto",
   "status.mention": "Nomina @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Espandi questo post",
+  "status.pin": "Pin on profile",
   "status.reblog": "Condividi",
   "status.reblogged_by": "{name} ha condiviso",
   "status.reply": "Rispondi",
@@ -179,6 +183,7 @@
   "status.show_less": "Mostra meno",
   "status.show_more": "Mostra di più",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Scrivi",
   "tabs_bar.federated_timeline": "Federazione",
   "tabs_bar.home": "Home",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 4c98086bb..560d2b668 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "本当に{name}をミュートしますか?",
   "confirmations.unfollow.confirm": "フォロー解除",
   "confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?",
+  "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
+  "embed.preview": "表示例:",
   "emoji_button.activity": "活動",
   "emoji_button.flags": "国旗",
   "emoji_button.food": "食べ物",
@@ -159,15 +161,17 @@
   "report.target": "{target} を通報する",
   "search.placeholder": "検索",
   "search_results.total": "{count, number}件の結果",
-  "standalone.public_title": "連合タイムライン",
+  "standalone.public_title": "今こんな話をしています",
   "status.cannot_reblog": "この投稿はブーストできません",
   "status.delete": "削除",
+  "status.embed": "埋め込み",
   "status.favourite": "お気に入り",
   "status.load_more": "もっと見る",
   "status.media_hidden": "非表示のメディア",
   "status.mention": "返信",
   "status.mute_conversation": "会話をミュート",
   "status.open": "詳細を表示",
+  "status.pin": "プロフィールに固定表示",
   "status.reblog": "ブースト",
   "status.reblogged_by": "{name}さんにブーストされました",
   "status.reply": "返信",
@@ -179,6 +183,7 @@
   "status.show_less": "隠す",
   "status.show_more": "もっと見る",
   "status.unmute_conversation": "会話のミュートを解除",
+  "status.unpin": "プロフィールの固定表示を解除",
   "tabs_bar.compose": "投稿",
   "tabs_bar.federated_timeline": "連合",
   "tabs_bar.home": "ホーム",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 47d0d4087..7d573506c 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "활동",
   "emoji_button.flags": "국기",
   "emoji_button.food": "음식",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
   "status.delete": "삭제",
+  "status.embed": "Embed",
   "status.favourite": "즐겨찾기",
   "status.load_more": "더 보기",
   "status.media_hidden": "미디어 숨겨짐",
   "status.mention": "답장",
   "status.mute_conversation": "이 대화를 뮤트",
   "status.open": "상세 정보 표시",
+  "status.pin": "Pin on profile",
   "status.reblog": "부스트",
   "status.reblogged_by": "{name}님이 부스트 했습니다",
   "status.reply": "답장",
@@ -179,6 +183,7 @@
   "status.show_less": "숨기기",
   "status.show_more": "더 보기",
   "status.unmute_conversation": "이 대화의 뮤트 해제하기",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "포스트",
   "tabs_bar.federated_timeline": "연합",
   "tabs_bar.home": "홈",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 4d68c7992..d6775e1e4 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
   "confirmations.unfollow.confirm": "Ontvolgen",
   "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activiteiten",
   "emoji_button.flags": "Vlaggen",
   "emoji_button.food": "Eten en drinken",
@@ -162,12 +164,14 @@
   "standalone.public_title": "Een kijkje binnenin...",
   "status.cannot_reblog": "Deze toot kan niet geboost worden",
   "status.delete": "Verwijderen",
+  "status.embed": "Embed",
   "status.favourite": "Favoriet",
   "status.load_more": "Meer laden",
   "status.media_hidden": "Media verborgen",
   "status.mention": "Vermeld @{name}",
   "status.mute_conversation": "Negeer conversatie",
   "status.open": "Toot volledig tonen",
+  "status.pin": "Pin on profile",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boostte",
   "status.reply": "Reageren",
@@ -179,6 +183,7 @@
   "status.show_less": "Minder tonen",
   "status.show_more": "Meer tonen",
   "status.unmute_conversation": "Conversatie niet meer negeren",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Schrijven",
   "tabs_bar.federated_timeline": "Globaal",
   "tabs_bar.home": "Start",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 9453e65ff..f3c24a807 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivitet",
   "emoji_button.flags": "Flagg",
   "emoji_button.food": "Mat og drikke",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "Denne posten kan ikke fremheves",
   "status.delete": "Slett",
+  "status.embed": "Embed",
   "status.favourite": "Lik",
   "status.load_more": "Last mer",
   "status.media_hidden": "Media skjult",
   "status.mention": "Nevn @{name}",
   "status.mute_conversation": "Demp samtale",
   "status.open": "Utvid denne statusen",
+  "status.pin": "Pin on profile",
   "status.reblog": "Fremhev",
   "status.reblogged_by": "Fremhevd av {name}",
   "status.reply": "Svar",
@@ -179,6 +183,7 @@
   "status.show_less": "Vis mindre",
   "status.show_more": "Vis mer",
   "status.unmute_conversation": "Ikke demp samtale",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Komponer",
   "tabs_bar.federated_timeline": "Felles",
   "tabs_bar.home": "Hjem",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index e2a5d7c59..141bff042 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -45,24 +45,26 @@
   "column_subheading.settings": "Paramètres",
   "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
   "compose_form.lock_disclaimer.lock": "clavat",
-  "compose_form.placeholder": "A de qué pensatz ?",
-  "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
+  "compose_form.placeholder": "A de qué pensatz ?",
+  "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
   "compose_form.publish": "Tut",
-  "compose_form.publish_loud": "{publish} !",
+  "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive": "Marcar lo mèdia coma sensible",
   "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
   "compose_form.spoiler_placeholder": "Escrivètz l’avertiment aquí",
   "confirmation_modal.cancel": "Anullar",
   "confirmations.block.confirm": "Blocar",
-  "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
+  "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
   "confirmations.delete.confirm": "Suprimir",
-  "confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?",
+  "confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
-  "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
+  "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
   "confirmations.mute.confirm": "Metre en silenci",
-  "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
+  "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
   "confirmations.unfollow.confirm": "Quitar de sègre",
-  "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
+  "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activitats",
   "emoji_button.flags": "Drapèus",
   "emoji_button.food": "Beure e manjar",
@@ -73,13 +75,13 @@
   "emoji_button.search": "Cercar…",
   "emoji_button.symbols": "Simbòls",
   "emoji_button.travel": "Viatges & lòcs",
-  "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
+  "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
   "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
   "empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.inactivity": "Vòstra pagina d’acuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
   "empty_column.home.public_timeline": "lo flux public",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
-  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
+  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Regetar",
   "getting_started.appsshort": "Apps",
@@ -109,19 +111,19 @@
   "navigation_bar.mutes": "Personas rescondudas",
   "navigation_bar.preferences": "Preferéncias",
   "navigation_bar.public_timeline": "Flux public global",
-  "notification.favourite": "{name} a ajustat a sos favorits :",
+  "notification.favourite": "{name} a ajustat a sos favorits :",
   "notification.follow": "{name} vos sèc",
-  "notification.mention": "{name} vos a mencionat :",
-  "notification.reblog": "{name} a partejat vòstre estatut :",
+  "notification.mention": "{name} vos a mencionat :",
+  "notification.reblog": "{name} a partejat vòstre estatut :",
   "notifications.clear": "Escafar",
-  "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
+  "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
   "notifications.column_settings.alert": "Notificacions localas",
-  "notifications.column_settings.favourite": "Favorits :",
-  "notifications.column_settings.follow": "Nòus seguidors :",
-  "notifications.column_settings.mention": "Mencions :",
+  "notifications.column_settings.favourite": "Favorits :",
+  "notifications.column_settings.follow": "Nòus seguidors :",
+  "notifications.column_settings.mention": "Mencions :",
   "notifications.column_settings.push": "Notificacions",
   "notifications.column_settings.push_meta": "Aqueste periferic",
-  "notifications.column_settings.reblog": "Partatges :",
+  "notifications.column_settings.reblog": "Partatges :",
   "notifications.column_settings.show": "Mostrar dins la colomna",
   "notifications.column_settings.sound": "Emetre un son",
   "onboarding.done": "Fach",
@@ -131,14 +133,14 @@
   "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
   "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.",
   "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
-  "onboarding.page_one.welcome": "Benvengut a Mastodon !",
+  "onboarding.page_one.welcome": "Benvengut a Mastodon !",
   "onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.",
   "onboarding.page_six.almost_done": "Gaireben acabat…",
   "onboarding.page_six.appetoot": "Bon Appetut!",
   "onboarding.page_six.apps_available": "I a d’aplicacions per mobil per iOS, Android e mai.",
   "onboarding.page_six.github": "Mastodon es un logicial liure e open-source.  Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
   "onboarding.page_six.guidelines": "guida de la comunitat",
-  "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} a {domain} !",
+  "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} a {domain} !",
   "onboarding.page_six.various_app": "aplicacions per mobil",
   "onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.",
   "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona d’una autra instància, picatz son identificant complet.",
@@ -162,14 +164,16 @@
   "standalone.public_title": "Una ulhada dedins…",
   "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
   "status.delete": "Escafar",
+  "status.embed": "Embed",
   "status.favourite": "Apondre als favorits",
   "status.load_more": "Cargar mai",
   "status.media_hidden": "Mèdia rescondut",
   "status.mention": "Mencionar",
   "status.mute_conversation": "Rescondre la conversacion",
   "status.open": "Desplegar aqueste estatut",
+  "status.pin": "Penjar al perfil",
   "status.reblog": "Partejar",
-  "status.reblogged_by": "{name} a partejat :",
+  "status.reblogged_by": "{name} a partejat :",
   "status.reply": "Respondre",
   "status.replyAll": "Respondre a la conversacion",
   "status.report": "Senhalar @{name}",
@@ -179,6 +183,7 @@
   "status.show_less": "Tornar plegar",
   "status.show_more": "Desplegar",
   "status.unmute_conversation": "Conversacions amb silenci levat",
+  "status.unpin": "Despenjar del perfil",
   "tabs_bar.compose": "Compausar",
   "tabs_bar.federated_timeline": "Flux public global",
   "tabs_bar.home": "Acuèlh",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 542230f11..e3e652970 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -10,7 +10,7 @@
   "account.media": "Media",
   "account.mention": "Wspomnij o @{name}",
   "account.mute": "Wycisz @{name}",
-  "account.posts": "Posty",
+  "account.posts": "Wpisy",
   "account.report": "Zgłoś @{name}",
   "account.requested": "Oczekująca prośba",
   "account.share": "Udostępnij profil @{name}",
@@ -43,10 +43,10 @@
   "column_header.unpin": "Cofnij przypięcie",
   "column_subheading.navigation": "Nawigacja",
   "column_subheading.settings": "Ustawienia",
-  "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje posty przeznaczone tylko dla śledzących.",
+  "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
   "compose_form.lock_disclaimer.lock": "zablokowane",
   "compose_form.placeholder": "Co Ci chodzi po głowie?",
-  "compose_form.privacy_disclaimer": "Twój post zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność postów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, post może być widoczny dla niewłaściwych osób.",
+  "compose_form.privacy_disclaimer": "Twój wpis zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność wpisów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, wpis może być widoczny dla niewłaściwych osób.",
   "compose_form.publish": "Wyślij",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Oznacz treści jako wrażliwe",
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
   "confirmations.unfollow.confirm": "Przestań śledzić",
   "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?",
+  "embed.instructions": "Osadź ten status na swojej stronie wklejając poniższy kod.",
+  "embed.preview": "Tak będzie to wyglądać:",
   "emoji_button.activity": "Aktywność",
   "emoji_button.flags": "Flagi",
   "emoji_button.food": "Żywność i napoje",
@@ -70,11 +72,11 @@
   "emoji_button.nature": "Natura",
   "emoji_button.objects": "Objekty",
   "emoji_button.people": "Ludzie",
-  "emoji_button.search": "Szukaj...",
+  "emoji_button.search": "Szukaj…",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Podróże i miejsca",
   "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
-  "empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
+  "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
   "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
   "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.",
   "empty_column.home.public_timeline": "publiczna oś czasu",
@@ -85,7 +87,7 @@
   "getting_started.appsshort": "Aplikacje",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Naucz się korzystać",
-  "getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj {github}.",
+  "getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
   "getting_started.userguide": "Podręcznik użytkownika",
   "home.column_settings.advanced": "Zaawansowane",
   "home.column_settings.basic": "Podstawowe",
@@ -96,7 +98,7 @@
   "lightbox.close": "Zamknij",
   "lightbox.next": "Następne",
   "lightbox.previous": "Poprzednie",
-  "loading_indicator.label": "Ładowanie...",
+  "loading_indicator.label": "Ładowanie…",
   "media_gallery.toggle_visible": "Przełącz widoczność",
   "missing_indicator.label": "Nie znaleziono",
   "navigation_bar.blocks": "Zablokowani użytkownicy",
@@ -116,12 +118,12 @@
   "notifications.clear": "Wyczyść powiadomienia",
   "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
   "notifications.column_settings.alert": "Powiadomienia na pulpicie",
-  "notifications.column_settings.favourite": "Ulubione:",
+  "notifications.column_settings.favourite": "Dodanie do ulubionych:",
   "notifications.column_settings.follow": "Nowi śledzący:",
-  "notifications.column_settings.mention": "Wspomniali:",
+  "notifications.column_settings.mention": "Wspomnienia:",
   "notifications.column_settings.push": "Powiadomienia push",
   "notifications.column_settings.push_meta": "To urządzenie",
-  "notifications.column_settings.reblog": "Podbili:",
+  "notifications.column_settings.reblog": "Podbicia:",
   "notifications.column_settings.show": "Pokaż w kolumnie",
   "notifications.column_settings.sound": "Odtwarzaj dźwięk",
   "onboarding.done": "Gotowe",
@@ -142,15 +144,15 @@
   "onboarding.page_six.various_app": "aplikacje mobilne",
   "onboarding.page_three.profile": "Edytuj profil, aby zmienić obraz profilowy, biografię, wyświetlaną nazwę i inne ustawienia.",
   "onboarding.page_three.search": "Użyj paska wyszukiwania aby znaleźć ludzi i hashtagi, takie jak {illustration} i {introductions}. Aby znaleźć osobę spoza tej instancji, musisz użyć pełnego adresu.",
-  "onboarding.page_two.compose": "Napisz posty, aby wypełnić kolumnę. Możesz wysłać zdjęcia, zmienić ustawienia prywatności lub dodać ostrzeżenie o zawartości.",
+  "onboarding.page_two.compose": "Utwórz wpisy, aby wypełnić kolumnę. Możesz wysłać zdjęcia, zmienić ustawienia prywatności lub dodać ostrzeżenie o zawartości.",
   "onboarding.skip": "Pomiń",
-  "privacy.change": "Dostosuj widoczność postów",
-  "privacy.direct.long": "Widoczne tylko dla oznaczonych",
+  "privacy.change": "Dostosuj widoczność wpisów",
+  "privacy.direct.long": "Widoczny tylko dla wspomnianych",
   "privacy.direct.short": "Bezpośrednio",
-  "privacy.private.long": "Widoczne tylko dla śledzących",
-  "privacy.private.short": "Tylko śledzący",
-  "privacy.public.long": "Widoczne na publicznych osiach czasu",
-  "privacy.public.short": "Publiczne",
+  "privacy.private.long": "Widoczny tylko dla osób, które Cię śledzą",
+  "privacy.private.short": "Tylko dla śledzących",
+  "privacy.public.long": "Widoczny na publicznych osiach czasu",
+  "privacy.public.short": "Publiczny",
   "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
   "privacy.unlisted.short": "Niewidoczne",
   "reply_indicator.cancel": "Anuluj",
@@ -160,14 +162,16 @@
   "search.placeholder": "Szukaj",
   "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
   "standalone.public_title": "Spojrzenie w głąb…",
-  "status.cannot_reblog": "Ten post nie może zostać podbity",
+  "status.cannot_reblog": "Ten wpis nie może zostać podbity",
   "status.delete": "Usuń",
+  "status.embed": "Osadź",
   "status.favourite": "Ulubione",
   "status.load_more": "Załaduj więcej",
   "status.media_hidden": "Zawartość multimedialna ukryta",
   "status.mention": "Wspomnij o @{name}",
   "status.mute_conversation": "Wycisz konwersację",
   "status.open": "Rozszerz ten status",
+  "status.pin": "Przypnij do profilu",
   "status.reblog": "Podbij",
   "status.reblogged_by": "{name} podbił",
   "status.reply": "Odpowiedz",
@@ -179,6 +183,7 @@
   "status.show_less": "Pokaż mniej",
   "status.show_more": "Pokaż więcej",
   "status.unmute_conversation": "Cofnij wyciszenie konwersacji",
+  "status.unpin": "Odepnij z profilu",
   "tabs_bar.compose": "Napisz",
   "tabs_bar.federated_timeline": "Globalne",
   "tabs_bar.home": "Strona główna",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 55d2f05de..e861bf73f 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -1,68 +1,70 @@
 {
   "account.block": "Bloquear @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.block_domain": "Esconder tudo de {domain}",
+  "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de maneira incompleta.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
   "account.follows": "Segue",
-  "account.follows_you": "É teu seguidor",
-  "account.media": "Media",
+  "account.follows_you": "É seu seguidor",
+  "account.media": "Mídia",
   "account.mention": "Mencionar @{name}",
   "account.mute": "Silenciar @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
-  "account.requested": "A aguardar aprovação",
-  "account.share": "Share @{name}'s profile",
+  "account.requested": "Aguardando aprovação",
+  "account.share": "Compartilhar perfil de @{name}",
   "account.unblock": "Não bloquear @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "Desbloquear {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
   "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.retry": "Tente novamente",
   "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.close": "Fechar",
   "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
-  "column.blocks": "Utilizadores Bloqueados",
+  "bundle_modal_error.retry": "Tente novamente",
+  "column.blocks": "Usuários bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
-  "column.follow_requests": "Seguidores Pendentes",
-  "column.home": "Home",
-  "column.mutes": "Utilizadores silenciados",
+  "column.follow_requests": "Seguidores pendentes",
+  "column.home": "Página inicial",
+  "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.public": "Global",
   "column_back_button.label": "Voltar",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
-  "column_subheading.navigation": "Navigation",
-  "column_subheading.settings": "Settings",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "column_header.hide_settings": "Esconder configurações",
+  "column_header.moveLeft_settings": "Mover coluna para a esquerda",
+  "column_header.moveRight_settings": "Mover coluna para a direita",
+  "column_header.pin": "Fixar",
+  "column_header.show_settings": "Mostrar configurações",
+  "column_header.unpin": "Desafixar",
+  "column_subheading.navigation": "Navegação",
+  "column_subheading.settings": "Configurações",
+  "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar as suas postagens só para seguidores.",
   "compose_form.lock_disclaimer.lock": "locked",
-  "compose_form.placeholder": "Em que estás a pensar?",
-  "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
+  "compose_form.placeholder": "No que você está pensando?",
+  "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários do {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com outros.",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marcar media como conteúdo sensível",
+  "compose_form.sensitive": "Marcar mídia como conteúdo sensível",
   "compose_form.spoiler": "Esconder texto com aviso",
   "compose_form.spoiler_placeholder": "Aviso de conteúdo",
-  "confirmation_modal.cancel": "Cancel",
-  "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
-  "confirmations.delete.confirm": "Delete",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
-  "confirmations.mute.confirm": "Mute",
-  "confirmations.mute.message": "Are you sure you want to mute {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "confirmation_modal.cancel": "Cancelar",
+  "confirmations.block.confirm": "Bloquear",
+  "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
+  "confirmations.delete.confirm": "Excluir",
+  "confirmations.delete.message": "Você tem certeza de que quer excluir este status?",
+  "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
+  "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
+  "confirmations.mute.confirm": "Silenciar",
+  "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
+  "confirmations.unfollow.confirm": "Deixar de seguir",
+  "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Eliminar",
+  "status.embed": "Embed",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
   "status.mention": "Mencionar @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expandir",
+  "status.pin": "Pin on profile",
   "status.reblog": "Partilhar",
   "status.reblogged_by": "{name} partilhou",
   "status.reply": "Responder",
@@ -179,6 +183,7 @@
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar mais",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Home",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 55d2f05de..f9e686411 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Eliminar",
+  "status.embed": "Embed",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
   "status.mention": "Mencionar @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expandir",
+  "status.pin": "Pin on profile",
   "status.reblog": "Partilhar",
   "status.reblogged_by": "{name} partilhou",
   "status.reply": "Responder",
@@ -179,6 +183,7 @@
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar mais",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Home",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 1abfb4370..0f78f4b17 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Блокировать",
   "account.block_domain": "Блокировать все с {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "Нижеуказанная информация может не полностью отражать профиль пользователя.",
   "account.edit_profile": "Изменить профиль",
   "account.follow": "Подписаться",
   "account.followers": "Подписаны",
@@ -13,19 +13,19 @@
   "account.posts": "Посты",
   "account.report": "Пожаловаться",
   "account.requested": "Ожидает подтверждения",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "Поделиться профилем @{name}",
   "account.unblock": "Разблокировать",
   "account.unblock_domain": "Разблокировать {domain}",
   "account.unfollow": "Отписаться",
   "account.unmute": "Снять глушение",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "Показать полный профиль",
   "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
+  "bundle_column_error.retry": "Попробовать снова",
+  "bundle_column_error.title": "Ошибка сети",
+  "bundle_modal_error.close": "Закрыть",
+  "bundle_modal_error.message": "Что-то пошло не так при загрузке этого компонента.",
+  "bundle_modal_error.retry": "Попробовать снова",
   "column.blocks": "Список блокировки",
   "column.community": "Локальная лента",
   "column.favourites": "Понравившееся",
@@ -35,11 +35,11 @@
   "column.notifications": "Уведомления",
   "column.public": "Глобальная лента",
   "column_back_button.label": "Назад",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.hide_settings": "Скрыть настройки",
+  "column_header.moveLeft_settings": "Передвинуть колонку влево",
+  "column_header.moveRight_settings": "Передвинуть колонку вправо",
   "column_header.pin": "Закрепить",
-  "column_header.show_settings": "Show settings",
+  "column_header.show_settings": "Показать настройки",
   "column_header.unpin": "Открепить",
   "column_subheading.navigation": "Навигация",
   "column_subheading.settings": "Настройки",
@@ -61,8 +61,10 @@
   "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
   "confirmations.mute.confirm": "Заглушить",
   "confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "confirmations.unfollow.confirm": "Отписаться",
+  "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Занятия",
   "emoji_button.flags": "Флаги",
   "emoji_button.food": "Еда и напитки",
@@ -94,8 +96,8 @@
   "home.column_settings.show_replies": "Показывать ответы",
   "home.settings": "Настройки колонки",
   "lightbox.close": "Закрыть",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "Далее",
+  "lightbox.previous": "Назад",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
   "missing_indicator.label": "Не найдено",
@@ -119,8 +121,8 @@
   "notifications.column_settings.favourite": "Нравится:",
   "notifications.column_settings.follow": "Новые подписчики:",
   "notifications.column_settings.mention": "Упоминания:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "Push-уведомления",
+  "notifications.column_settings.push_meta": "Это устройство",
   "notifications.column_settings.reblog": "Продвижения:",
   "notifications.column_settings.show": "Показывать в колонке",
   "notifications.column_settings.sound": "Проигрывать звук",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "Этот статус не может быть продвинут",
   "status.delete": "Удалить",
+  "status.embed": "Embed",
   "status.favourite": "Нравится",
   "status.load_more": "Показать еще",
   "status.media_hidden": "Медиаконтент скрыт",
   "status.mention": "Упомянуть @{name}",
   "status.mute_conversation": "Заглушить тред",
   "status.open": "Развернуть статус",
+  "status.pin": "Pin on profile",
   "status.reblog": "Продвинуть",
   "status.reblogged_by": "{name} продвинул(а)",
   "status.reply": "Ответить",
@@ -179,6 +183,7 @@
   "status.show_less": "Свернуть",
   "status.show_more": "Развернуть",
   "status.unmute_conversation": "Снять глушение с треда",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Написать",
   "tabs_bar.federated_timeline": "Глобальная",
   "tabs_bar.home": "Главная",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index aa0929f82..069fdf7c3 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Are you sure you want to mute {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.delete": "Delete",
+  "status.embed": "Embed",
   "status.favourite": "Favourite",
   "status.load_more": "Load more",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mention @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boosted",
   "status.reply": "Reply",
@@ -179,6 +183,7 @@
   "status.show_less": "Show less",
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Compose",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Home",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 37ce8597e..8a36bd207 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "{name} kullanıcısını sessize almak istiyor musunuz?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivite",
   "emoji_button.flags": "Bayraklar",
   "emoji_button.food": "Yiyecek ve İçecek",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "Bu gönderi boost edilemez",
   "status.delete": "Sil",
+  "status.embed": "Embed",
   "status.favourite": "Favorilere ekle",
   "status.load_more": "Daha fazla",
   "status.media_hidden": "Gizli görsel",
   "status.mention": "Bahset @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "Bu gönderiyi genişlet",
+  "status.pin": "Pin on profile",
   "status.reblog": "Boost'la",
   "status.reblogged_by": "{name} boost etti",
   "status.reply": "Cevapla",
@@ -179,6 +183,7 @@
   "status.show_less": "Daha azı",
   "status.show_more": "Daha fazlası",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Oluştur",
   "tabs_bar.federated_timeline": "Federe",
   "tabs_bar.home": "Ana sayfa",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index fea7bd94e..1d06218e6 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Заняття",
   "emoji_button.flags": "Прапори",
   "emoji_button.food": "Їжа та напої",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "Цей допис не може бути передмухнутий",
   "status.delete": "Видалити",
+  "status.embed": "Embed",
   "status.favourite": "Подобається",
   "status.load_more": "Завантажити більше",
   "status.media_hidden": "Медіаконтент приховано",
   "status.mention": "Згадати",
   "status.mute_conversation": "Заглушити діалог",
   "status.open": "Розгорнути допис",
+  "status.pin": "Pin on profile",
   "status.reblog": "Передмухнути",
   "status.reblogged_by": "{name} передмухнув(-ла)",
   "status.reply": "Відповісти",
@@ -179,6 +183,7 @@
   "status.show_less": "Згорнути",
   "status.show_more": "Розгорнути",
   "status.unmute_conversation": "Зняти глушення з діалогу",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "Написати",
   "tabs_bar.federated_timeline": "Глобальна",
   "tabs_bar.home": "Головна",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index d0c4b3d1b..93faf8876 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -5,7 +5,7 @@
   "account.edit_profile": "修改个人资料",
   "account.follow": "关注",
   "account.followers": "关注者",
-  "account.follows": "正关注",
+  "account.follows": "正在关注",
   "account.follows_you": "关注你",
   "account.media": "Media",
   "account.mention": "提及 @{name}",
@@ -13,19 +13,19 @@
   "account.posts": "嘟文",
   "account.report": "举报 @{name}",
   "account.requested": "等待审批",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "分享 @{name}的个人资料",
   "account.unblock": "解除对 @{name} 的屏蔽",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "解除封锁 {domain}",
   "account.unfollow": "取消关注",
   "account.unmute": "取消 @{name} 的静音",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "查看完整资料",
   "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "载入组件出错。",
+  "bundle_column_error.retry": "再次尝试",
+  "bundle_column_error.title": "网络错误",
+  "bundle_modal_error.close": "关闭",
+  "bundle_modal_error.message": "载入组件出错。",
+  "bundle_modal_error.retry": "再次尝试",
   "column.blocks": "屏蔽用户",
   "column.community": "本站时间轴",
   "column.favourites": "赞过的嘟文",
@@ -34,7 +34,7 @@
   "column.mutes": "被静音的用户",
   "column.notifications": "通知",
   "column.public": "跨站公共时间轴",
-  "column_back_button.label": "Back",
+  "column_back_button.label": "返回",
   "column_header.hide_settings": "Hide settings",
   "column_header.moveLeft_settings": "Move column to the left",
   "column_header.moveRight_settings": "Move column to the right",
@@ -61,8 +61,10 @@
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "静音",
   "confirmations.mute.message": "想好了,真的要静音 {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "confirmations.unfollow.confirm": "取消关注",
+  "confirmations.unfollow.message": "确定要取消关注 {name}吗?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "活动",
   "emoji_button.flags": "旗帜",
   "emoji_button.food": "食物和饮料",
@@ -86,7 +88,7 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "开始使用",
   "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
-  "getting_started.userguide": "User Guide",
+  "getting_started.userguide": "用户指南",
   "home.column_settings.advanced": "高端",
   "home.column_settings.basic": "基本",
   "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "没法转嘟这条嘟文啦……",
   "status.delete": "删除",
+  "status.embed": "Embed",
   "status.favourite": "赞",
   "status.load_more": "加载更多",
   "status.media_hidden": "隐藏媒体内容",
   "status.mention": "提及 @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "展开嘟文",
+  "status.pin": "Pin on profile",
   "status.reblog": "转嘟",
   "status.reblogged_by": "{name} 转嘟",
   "status.reply": "回应",
@@ -179,6 +183,7 @@
   "status.show_less": "减少显示",
   "status.show_more": "显示更多",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "撰写",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主页",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 7312aae82..d689cd5ae 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "你確定要將{name}靜音嗎?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "活動",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "飲飲食食",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "這篇文章無法被轉推",
   "status.delete": "刪除",
+  "status.embed": "Embed",
   "status.favourite": "喜歡",
   "status.load_more": "載入更多",
   "status.media_hidden": "隱藏媒體內容",
   "status.mention": "提及 @{name}",
   "status.mute_conversation": "Mute conversation",
   "status.open": "展開文章",
+  "status.pin": "Pin on profile",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推",
   "status.reply": "回應",
@@ -179,6 +183,7 @@
   "status.show_less": "減少顯示",
   "status.show_more": "顯示更多",
   "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "撰寫",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主頁",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 1c2e35272..dcb9d7f3c 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -63,6 +63,8 @@
   "confirmations.mute.message": "你確定要消音 {name} ?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "活動",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "食物與飲料",
@@ -162,12 +164,14 @@
   "standalone.public_title": "A look inside...",
   "status.cannot_reblog": "此貼文無法轉推",
   "status.delete": "刪除",
+  "status.embed": "Embed",
   "status.favourite": "喜愛",
   "status.load_more": "載入更多",
   "status.media_hidden": "媒體已隱藏",
   "status.mention": "提到 @{name}",
   "status.mute_conversation": "消音對話",
   "status.open": "展開這個狀態",
+  "status.pin": "Pin on profile",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推了",
   "status.reply": "回應",
@@ -179,6 +183,7 @@
   "status.show_less": "看少點",
   "status.show_more": "看更多",
   "status.unmute_conversation": "不消音對話",
+  "status.unpin": "Unpin from profile",
   "tabs_bar.compose": "編輯",
   "tabs_bar.federated_timeline": "聯盟",
   "tabs_bar.home": "家",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 07207c93b..e7a3567b4 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -149,10 +149,20 @@ const privacyPreference = (a, b) => {
   }
 };
 
+const hydrate = (state, hydratedState) => {
+  state = clearAll(state.merge(hydratedState));
+
+  if (hydratedState.has('text')) {
+    state = state.set('text', hydratedState.get('text'));
+  }
+
+  return state;
+};
+
 export default function compose(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
-    return clearAll(state.merge(action.state.get('compose')));
+    return hydrate(state, action.state.get('compose'));
   case COMPOSE_MOUNT:
     return state.set('mounted', true);
   case COMPOSE_UNMOUNT:
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index bbc973302..2ce27a454 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -3,6 +3,10 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import {
+  FAVOURITE_SUCCESS,
+  UNFAVOURITE_SUCCESS,
+} from '../actions/interactions';
 
 const initialState = ImmutableMap({
   favourites: ImmutableMap({
@@ -27,12 +31,28 @@ const appendToList = (state, listType, statuses, next) => {
   }));
 };
 
+const prependOneToList = (state, listType, status) => {
+  return state.update(listType, listMap => listMap.withMutations(map => {
+    map.set('items', map.get('items').unshift(status.get('id')));
+  }));
+};
+
+const removeOneFromList = (state, listType, status) => {
+  return state.update(listType, listMap => listMap.withMutations(map => {
+    map.set('items', map.get('items').filter(item => item !== status.get('id')));
+  }));
+};
+
 export default function statusLists(state = initialState, action) {
   switch(action.type) {
   case FAVOURITED_STATUSES_FETCH_SUCCESS:
     return normalizeList(state, 'favourites', action.statuses, action.next);
   case FAVOURITED_STATUSES_EXPAND_SUCCESS:
     return appendToList(state, 'favourites', action.statuses, action.next);
+  case FAVOURITE_SUCCESS:
+    return prependOneToList(state, 'favourites', action.status);
+  case UNFAVOURITE_SUCCESS:
+    return removeOneFromList(state, 'favourites', action.status);
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 3e40b0b42..38691dc43 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -7,6 +7,8 @@ import {
   FAVOURITE_SUCCESS,
   FAVOURITE_FAIL,
   UNFAVOURITE_SUCCESS,
+  PIN_SUCCESS,
+  UNPIN_SUCCESS,
 } from '../actions/interactions';
 import {
   STATUS_FETCH_SUCCESS,
@@ -114,6 +116,8 @@ export default function statuses(state = initialState, action) {
   case UNREBLOG_SUCCESS:
   case FAVOURITE_SUCCESS:
   case UNFAVOURITE_SUCCESS:
+  case PIN_SUCCESS:
+  case UNPIN_SUCCESS:
     return normalizeStatus(state, action.response);
   case FAVOURITE_REQUEST:
     return state.setIn([action.status.get('id'), 'favourited'], true);
diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/mastodon/scroll.js
index 44f95b17f..2af07e0fb 100644
--- a/app/javascript/mastodon/scroll.js
+++ b/app/javascript/mastodon/scroll.js
@@ -26,5 +26,5 @@ const scroll = (node, key, target) => {
   };
 };
 
-export const scrollRight = (node) => scroll(node, 'scrollLeft', node.scrollWidth);
+export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position);
 export const scrollTop = (node) => scroll(node, 'scrollTop', 0);
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index acb85f626..f63cff335 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -31,8 +31,8 @@ const notify = options =>
       const group = cloneNotification(notifications[0]);
 
       group.title = formatGroupTitle(group.data.message, group.data.count + 1);
-      group.body = `${options.title}\n${group.body}`;
-      group.data = { ...group.data, count: group.data.count + 1 };
+      group.body  = `${options.title}\n${group.body}`;
+      group.data  = { ...group.data, count: group.data.count + 1 };
 
       return self.registration.showNotification(group.title, group);
     }
@@ -43,18 +43,18 @@ const notify = options =>
 const handlePush = (event) => {
   const options = event.data.json();
 
-  options.body = options.data.nsfw || options.data.content;
-  options.image = options.image || undefined; // Null results in a network request (404)
+  options.body      = options.data.nsfw || options.data.content;
+  options.dir       = options.data.dir;
+  options.image     = options.image || undefined; // Null results in a network request (404)
   options.timestamp = options.timestamp && new Date(options.timestamp);
 
   const expandAction = options.data.actions.find(action => action.todo === 'expand');
 
   if (expandAction) {
-    options.actions = [expandAction];
-    options.hiddenActions = options.data.actions.filter(action => action !== expandAction);
-
+    options.actions          = [expandAction];
+    options.hiddenActions    = options.data.actions.filter(action => action !== expandAction);
     options.data.hiddenImage = options.image;
-    options.image = undefined;
+    options.image            = undefined;
   } else {
     options.actions = options.data.actions;
   }
@@ -75,8 +75,8 @@ const cloneNotification = (notification) => {
 const expandNotification = (notification) => {
   const nextNotification = cloneNotification(notification);
 
-  nextNotification.body = notification.data.content;
-  nextNotification.image = notification.data.hiddenImage;
+  nextNotification.body    = notification.data.content;
+  nextNotification.image   = notification.data.hiddenImage;
   nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand');
 
   return self.registration.showNotification(nextNotification.title, nextNotification);
@@ -105,8 +105,7 @@ const openUrl = url =>
       const webClients = clientList.filter(client => /\/web\//.test(client.url));
 
       if (webClients.length !== 0) {
-        const client = findBestClient(webClients);
-
+        const client       = findBestClient(webClients);
         const { pathname } = new URL(url);
 
         if (pathname.startsWith('/web/')) {
@@ -126,8 +125,7 @@ const openUrl = url =>
   });
 
 const removeActionFromNotification = (notification, action) => {
-  const actions = notification.actions.filter(act => act.action !== action.action);
-
+  const actions          = notification.actions.filter(act => act.action !== action.action);
   const nextNotification = cloneNotification(notification);
 
   nextNotification.actions = actions;
diff --git a/app/javascript/mastodon/web_push_subscription.js b/app/javascript/mastodon/web_push_subscription.js
index 96ac63b52..3dbed09ea 100644
--- a/app/javascript/mastodon/web_push_subscription.js
+++ b/app/javascript/mastodon/web_push_subscription.js
@@ -48,7 +48,6 @@ export function register () {
 
   if (supportsPushNotifications) {
     if (!getApplicationServerKey()) {
-      // eslint-disable-next-line no-console
       console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
       return;
     }
@@ -84,10 +83,8 @@ export function register () {
       })
       .catch(error => {
         if (error.code === 20 && error.name === 'AbortError') {
-          // eslint-disable-next-line no-console
           console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
         } else if (error.code === 5 && error.name === 'InvalidCharacterError') {
-          // eslint-disable-next-line no-console
           console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
         }
 
@@ -103,7 +100,6 @@ export function register () {
         }
       });
   } else {
-    // eslint-disable-next-line no-console
     console.warn('Your browser does not support Web Push Notifications.');
   }
 }
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index e9bb4a42e..2058fad91 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -36,8 +36,20 @@ function main() {
 
     [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
       const datetime = new Date(content.getAttribute('datetime'));
+      content.title = dateTimeFormat.format(datetime);
       content.textContent = relativeFormat.format(datetime);
     });
+
+    [].forEach.call(document.querySelectorAll('.logo-button'), (content) => {
+      content.addEventListener('click', (e) => {
+        e.preventDefault();
+        window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
+      });
+    });
+
+    if (window.parent) {
+      window.parent.postMessage(['setHeight', document.getElementsByTagName('html')[0].scrollHeight], '*');
+    }
   });
 
   delegate(document, '.video-player video', 'click', ({ target }) => {
diff --git a/app/javascript/packs/share.js b/app/javascript/packs/share.js
new file mode 100644
index 000000000..51e4ae38b
--- /dev/null
+++ b/app/javascript/packs/share.js
@@ -0,0 +1,24 @@
+import loadPolyfills from '../mastodon/load_polyfills';
+
+require.context('../images/', true);
+
+function loaded() {
+  const ComposeContainer = require('../mastodon/containers/compose_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+  const mountNode = document.getElementById('mastodon-compose');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<ComposeContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('../mastodon/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss
index 66da44086..28924738a 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/about.scss
@@ -1,52 +1,96 @@
-.about-body {
-  .wrapper {
-    max-width: 600px;
-    margin: 0 auto;
+.landing-page {
+  p,
+  li {
+    font-family: 'mastodon-font-sans-serif', sans-serif;
+    font-size: 16px;
+    font-weight: 400;
+    font-size: 16px;
+    line-height: 30px;
+    margin-bottom: 12px;
     color: $ui-primary-color;
-    padding-top: 50px;
-    padding-bottom: 50px;
 
-    &.thicc {
-      max-width: 800px;
+    a {
+      color: $ui-highlight-color;
+      text-decoration: underline;
     }
   }
 
+  em {
+    display: inline;
+    margin: 0;
+    padding: 0;
+    font-weight: 500;
+    background: transparent;
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    color: lighten($ui-primary-color, 10%);
+  }
+
   h1 {
-    font: 46px/52px 'mastodon-font-sans-serif', sans-serif;
-    font-weight: 600;
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 26px;
+    line-height: 30px;
+    font-weight: 500;
     margin-bottom: 20px;
-    color: $ui-highlight-color;
-    padding: 20px 0;
+    color: $ui-secondary-color;
 
-    img {
-      margin-bottom: -5px;
-      margin-right: 5px;
-      width: 46px;
-      height: 46px;
+    small {
+      font-family: 'mastodon-font-sans-serif', sans-serif;
+      display: block;
+      font-size: 18px;
+      font-weight: 400;
+      color: $ui-base-lighter-color;
     }
   }
 
   h2 {
     font-family: 'mastodon-font-display', sans-serif;
-    font-size: 24px;
-    line-height: 28px;
-    font-weight: 400;
+    font-size: 22px;
+    line-height: 26px;
+    font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $ui-secondary-color;
   }
 
   h3 {
     font-family: 'mastodon-font-display', sans-serif;
-    font-size: 20px;
-    line-height: 28px;
-    font-weight: 400;
+    font-size: 18px;
+    line-height: 24px;
+    font-weight: 500;
+    margin-bottom: 20px;
+    color: $ui-secondary-color;
+  }
+
+  h4 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 16px;
+    line-height: 24px;
+    font-weight: 500;
+    margin-bottom: 20px;
+    color: $ui-secondary-color;
+  }
+
+  h5 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 14px;
+    line-height: 24px;
+    font-weight: 500;
+    margin-bottom: 20px;
+    color: $ui-secondary-color;
+  }
+
+  h6 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 12px;
+    line-height: 24px;
+    font-weight: 500;
     margin-bottom: 20px;
     color: $ui-secondary-color;
   }
 
   ul,
   ol {
-    list-style: inherit;
     margin-left: 20px;
 
     &[type='a'] {
@@ -58,219 +102,29 @@
     }
   }
 
-  li > ol,
-  li > ul {
-    margin-top: 20px;
-  }
-
-  p,
-  li {
-    font: 16px/28px 'mastodon-font-sans-serif', sans-serif;
-    font-weight: 400;
-    margin-bottom: 12px;
-
-    a {
-      color: $ui-highlight-color;
-      text-decoration: underline;
-    }
-  }
-
-  em {
-    display: inline-block;
-    padding: 7px 7px 5px;
-    margin: 0 2px;
-    background: $ui-primary-color;
-    color: $ui-base-color;
-    font: 16px/16px 'mastodon-font-sans-serif', sans-serif;
-    font-weight: 300;
-  }
-
-  .screenshot {
-    box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
-    margin-bottom: 26px;
-
-    img {
-      max-width: 100%;
-      height: auto;
-      display: block;
-    }
+  ul {
+    list-style: disc;
   }
 
-  .actions {
-    overflow: hidden;
-    margin-bottom: 20px;
-
-    .info {
-      float: right;
-      text-align: right;
-      line-height: 36px;
-
-      a {
-        color: $ui-primary-color;
-        text-decoration: underline;
-      }
-    }
+  ol {
+    list-style: decimal;
   }
 
-  @media screen and (max-width: 625px) {
-    .wrapper {
-      padding: 20px;
-    }
+  li > ol,
+  li > ul {
+    margin-top: 6px;
   }
-}
-
-.information-board {
-  background: darken($ui-base-color, 4%);
-  padding: 20px 0;
-
-  .panel {
-    position: absolute;
-    width: 280px;
-    box-sizing: border-box;
-    background: darken($ui-base-color, 8%);
-    padding: 20px;
-    padding-top: 10px;
-    border-radius: 4px 4px 0 0;
-    right: 0;
-    bottom: -40px;
-
-    .panel-header {
-      font-family: 'mastodon-font-display', sans-serif;
-      font-size: 14px;
-      line-height: 24px;
-      font-weight: 500;
-      color: $ui-base-lighter-color;
-      padding-bottom: 5px;
-      margin-bottom: 15px;
-      border-bottom: 1px solid lighten($ui-base-color, 4%);
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      overflow: hidden;
-
-      a,
-      span {
-        font-weight: 400;
-        color: lighten($ui-base-color, 34%);
-      }
 
-      a {
-        text-decoration: none;
-      }
-    }
+  hr {
+    border-color: rgba($ui-base-lighter-color, .6);
   }
 
   .container {
-    position: relative;
-    padding-right: 280px + 15px;
-  }
-
-  .information-board-sections {
-    display: flex;
-    justify-content: space-between;
-    flex-wrap: wrap;
-  }
-
-  .section {
-    flex: 1 0 0;
-    font: 16px/28px 'mastodon-font-sans-serif', sans-serif;
-    text-align: right;
-    padding: 10px 15px;
-
-    span,
-    strong {
-      display: block;
-    }
-
-    span {
-      font-size: 16px;
-
-      &:last-child {
-        color: $ui-secondary-color;
-      }
-    }
-
-    strong {
-      font-weight: 500;
-      font-size: 32px;
-      line-height: 48px;
-      color: $primary-text-color;
-    }
-  }
-}
-
-.owner {
-  text-align: center;
-
-  .avatar {
-    @include avatar-size(80px);
+    width: 100%;
+    box-sizing: border-box;
+    max-width: 800px;
     margin: 0 auto;
-    margin-bottom: 15px;
-
-    img {
-      @include avatar-radius();
-      @include avatar-size(80px);
-      display: block;
-    }
-  }
-
-  .name {
-    font-size: 14px;
-
-    a {
-      display: block;
-      color: $primary-text-color;
-      text-decoration: none;
-
-      &:hover {
-        .display_name {
-          text-decoration: underline;
-        }
-      }
-    }
-
-    .username {
-      display: block;
-      color: $ui-primary-color;
-    }
-  }
-}
-
-.features-list__row {
-  display: flex;
-  padding: 10px 0;
-  justify-content: space-between;
-
-  &:first-child {
-    padding-top: 0;
-  }
-
-  .visual {
-    flex: 0 0 auto;
-    display: flex;
-    align-items: center;
-    margin-left: 15px;
-
-    .fa {
-      display: block;
-      color: $ui-primary-color;
-      font-size: 48px;
-    }
-  }
-
-  .text {
-    font-size: 16px;
-    line-height: 30px;
-    color: $ui-base-lighter-color;
-
-    h6 {
-      font-weight: 500;
-      color: $ui-primary-color;
-    }
   }
-}
-
-.landing-page {
-  $lp-par-color: lighten($ui-base-color, 36%);
 
   .header-wrapper {
     padding-top: 15px;
@@ -284,13 +138,16 @@
 
       .hero .heading {
         padding-bottom: 30px;
+        font-family: 'mastodon-font-sans-serif', sans-serif;
+        font-size: 16px;
+        font-weight: 400;
+        font-size: 16px;
+        line-height: 30px;
+        color: $ui-primary-color;
 
-        p, li {
-          color: lighten($ui-base-color, 50%);
-        }
-
-        li {
-          margin: 2px 0;
+        a {
+          color: $ui-highlight-color;
+          text-decoration: underline;
         }
       }
     }
@@ -315,17 +172,6 @@
     }
   }
 
-  p,
-  li {
-    font: inherit;
-    font-weight: inherit;
-    margin-bottom: 0;
-  }
-
-  hr {
-    border-color: rgba($ui-base-lighter-color, .6);
-  }
-
   .header {
     line-height: 30px;
     overflow: hidden;
@@ -335,6 +181,62 @@
       justify-content: space-between;
     }
 
+    .links {
+      position: relative;
+      z-index: 4;
+
+      a {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        color: $ui-primary-color;
+        text-decoration: none;
+        padding: 12px 16px;
+        line-height: 32px;
+        font-family: 'mastodon-font-display', sans-serif;
+        font-weight: 500;
+        font-size: 14px;
+
+        &:hover {
+          color: $ui-secondary-color;
+        }
+      }
+
+      .brand {
+        a {
+          padding-left: 0;
+          padding-right: 0;
+          color: $white;
+        }
+
+        img {
+          height: 32px;
+          position: relative;
+          top: 4px;
+          left: -10px;
+        }
+      }
+
+      ul {
+        list-style: none;
+        margin: 0;
+
+        li {
+          display: inline-block;
+          vertical-align: bottom;
+          margin: 0;
+
+          &:first-child a {
+            padding-left: 0;
+          }
+
+          &:last-child a {
+            padding-right: 0;
+          }
+        }
+      }
+    }
+
     .hero {
       margin-top: 50px;
       align-items: center;
@@ -387,6 +289,12 @@
         }
       }
 
+      .heading {
+        position: relative;
+        z-index: 4;
+        padding-bottom: 150px;
+      }
+
       .simple_form,
       .closed-registrations-message {
         background: darken($ui-base-color, 4%);
@@ -408,12 +316,6 @@
         }
       }
 
-      .heading {
-        position: relative;
-        z-index: 4;
-        padding-bottom: 150px;
-      }
-
       .closed-registrations-message {
         min-height: 330px;
         display: flex;
@@ -421,136 +323,140 @@
         justify-content: space-between;
       }
     }
+  }
 
-    .links {
+  .about-short {
+    background: darken($ui-base-color, 4%);
+    padding: 50px 0;
+    font-family: 'mastodon-font-sans-serif', sans-serif;
+    font-size: 16px;
+    font-weight: 400;
+    font-size: 16px;
+    line-height: 30px;
+    color: $ui-primary-color;
+
+    a {
+      color: $ui-highlight-color;
+      text-decoration: underline;
+    }
+  }
+
+  .information-board {
+    background: darken($ui-base-color, 4%);
+    padding: 20px 0;
+
+    .container {
       position: relative;
-      z-index: 4;
+      padding-right: 280px + 15px;
+    }
 
-      ul {
-        list-style: none;
-        margin: 0;
+    .information-board-sections {
+      display: flex;
+      justify-content: space-between;
+      flex-wrap: wrap;
+    }
 
-        li {
-          display: inline-block;
-          vertical-align: bottom;
-          margin: 0;
+    .section {
+      flex: 1 0 0;
+      font-family: 'mastodon-font-sans-serif', sans-serif;
+      font-size: 16px;
+      line-height: 28px;
+      color: $primary-text-color;
+      text-align: right;
+      padding: 10px 15px;
 
-          &:last-child a {
-            padding-right: 0;
-          }
-        }
+      span,
+      strong {
+        display: block;
       }
 
-      a {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        color: $ui-primary-color;
-        text-decoration: none;
-        padding: 12px 16px;
-        line-height: 32px;
-        font-family: 'mastodon-font-display', sans-serif;
-        font-weight: 500;
-        font-size: 14px;
-
-        &:hover {
+      span {
+        &:last-child {
           color: $ui-secondary-color;
         }
       }
 
-      .brand {
-        a {
-          padding-left: 0;
-          padding-right: 0;
-          color: $white;
-        }
-
-        img {
-          height: 32px;
-          position: relative;
-          top: 4px;
-          left: -10px;
-        }
+      strong {
+        font-weight: 500;
+        font-size: 32px;
+        line-height: 48px;
       }
     }
-  }
-
-  .container {
-    width: 100%;
-    box-sizing: border-box;
-    max-width: 800px;
-    margin: 0 auto;
-  }
-
-  .wrapper {
-    max-width: 800px;
-    margin: 0 auto;
-    padding: 0;
-  }
-
-  .learn-more-cta, .extended-description {
-    padding: 50px 0;
-    font-weight: 400;
-    color: $lp-par-color;
-    font: 16px/1.6 'mastodon-font-sans-serif', sans-serif;
 
-    ul,
-    ol {
-      list-style: inherit;
-      margin-left: 20px;
+    .panel {
+      position: absolute;
+      width: 280px;
+      box-sizing: border-box;
+      background: darken($ui-base-color, 8%);
+      padding: 20px;
+      padding-top: 10px;
+      border-radius: 4px 4px 0 0;
+      right: 0;
+      bottom: -40px;
 
-      &[type='a'] {
-        list-style-type: lower-alpha;
-      }
+      .panel-header {
+        font-family: 'mastodon-font-display', sans-serif;
+        font-size: 14px;
+        line-height: 24px;
+        font-weight: 500;
+        color: $ui-primary-color;
+        padding-bottom: 5px;
+        margin-bottom: 15px;
+        border-bottom: 1px solid lighten($ui-base-color, 4%);
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        overflow: hidden;
+
+        a,
+        span {
+          font-weight: 400;
+          color: darken($ui-primary-color, 10%);
+        }
 
-      &[type='i'] {
-        list-style-type: lower-roman;
+        a {
+          text-decoration: none;
+        }
       }
     }
 
-    li > ol,
-    li > ul {
-      margin-top: 20px;
-    }
+    .owner {
+      text-align: center;
 
-    p,
-    li {
-      color: $lp-par-color;
-      margin-bottom: 6px;
+      .avatar {
+        width: 80px;
+        height: 80px;
+        margin: 0 auto;
+        margin-bottom: 15px;
 
-      a {
-        color: $ui-highlight-color;
-        text-decoration: underline;
+        img {
+          display: block;
+          width: 80px;
+          height: 80px;
+          border-radius: 48px;
+        }
       }
-    }
-
-    li {
-      margin: 2px 0;
-    }
-  }
 
-  .learn-more-cta {
-    background: darken($ui-base-color, 4%);
-    padding: 50px 0;
-    p {
-      font-size: 16px;
-      line-height: 28px;
-    }
-  }
+      .name {
+        font-size: 14px;
 
-  h3 {
-    font-family: 'mastodon-font-display', sans-serif;
-    font-size: 16px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $ui-primary-color;
-  }
+        a {
+          display: block;
+          color: $primary-text-color;
+          text-decoration: none;
+
+          &:hover {
+            .display_name {
+              text-decoration: underline;
+            }
+          }
+        }
 
-  p {
-    font-size: 16px;
-    line-height: 28px;
-    color: $lp-par-color;
+        .username {
+          display: block;
+          color: $ui-primary-color;
+        }
+      }
+    }
   }
 
   .features {
@@ -559,100 +465,121 @@
     .container {
       display: flex;
     }
-  }
 
-  #mastodon-timeline {
-    display: flex;
-    -webkit-overflow-scrolling: touch;
-    -ms-overflow-style: -ms-autohiding-scrollbar;
-    font-family: 'mastodon-font-sans-serif', sans-serif;
-    font-size: 13px;
-    line-height: 18px;
-    font-weight: 400;
-    color: $primary-text-color;
-    width: 330px;
-    margin-right: 30px;
-    flex: 0 0 auto;
-    background: $ui-base-color;
-    overflow: hidden;
-    box-shadow: 0 0 6px rgba($black, 0.1);
+    #mastodon-timeline {
+      display: flex;
+      -webkit-overflow-scrolling: touch;
+      -ms-overflow-style: -ms-autohiding-scrollbar;
+      font-family: 'mastodon-font-sans-serif', sans-serif;
+      font-size: 13px;
+      line-height: 18px;
+      font-weight: 400;
+      color: $primary-text-color;
+      width: 330px;
+      margin-right: 30px;
+      flex: 0 0 auto;
+      background: $ui-base-color;
+      overflow: hidden;
+      box-shadow: 0 0 6px rgba($black, 0.1);
+
+      .column-header {
+        color: inherit;
+        font-family: inherit;
+        font-size: 16px;
+        line-height: inherit;
+        font-weight: inherit;
+        margin: 0;
+        padding: 15px;
+      }
 
-    .column-header {
-      color: inherit;
-      font-family: inherit;
-      font-size: 16px;
-      line-height: inherit;
-      font-weight: inherit;
-      margin: 0;
-      padding: 15px;
-    }
+      .column {
+        padding: 0;
+        border-radius: 4px;
+        overflow: hidden;
+      }
 
-    .column {
-      padding: 0;
-      border-radius: 4px;
-      overflow: hidden;
-    }
+      .scrollable {
+        height: 400px;
+      }
 
-    .scrollable {
-      height: 400px;
-    }
+      p {
+        font-size: inherit;
+        line-height: inherit;
+        font-weight: inherit;
+        color: $primary-text-color;
+        margin-bottom: 20px;
 
-    p {
-      font-size: inherit;
-      line-height: inherit;
-      font-weight: inherit;
-      color: $primary-text-color;
-      margin-bottom: 20px;
+        &:last-child {
+          margin-bottom: 0;
+        }
 
-      &:last-child {
-        margin-bottom: 0;
+        a {
+          color: $ui-secondary-color;
+          text-decoration: none;
+        }
       }
+    }
 
-      a {
-        color: $ui-secondary-color;
-        text-decoration: none;
+    .about-mastodon {
+      max-width: 675px;
+
+      p {
+        margin-bottom: 20px;
       }
-    }
-  }
 
-  .about-mastodon {
-    max-width: 675px;
+      .features-list {
+        margin-top: 20px;
 
-    p {
-      margin-bottom: 20px;
-    }
+        .features-list__row {
+          display: flex;
+          padding: 10px 0;
+          justify-content: space-between;
 
-    .features-list {
-      margin-top: 20px;
-    }
-  }
+          &:first-child {
+            padding-top: 0;
+          }
 
-  em {
-    display: inline;
-    margin: 0;
-    padding: 0;
-    font-weight: 500;
-    background: transparent;
-    font-family: inherit;
-    font-size: inherit;
-    line-height: inherit;
-    color: $ui-primary-color;
+          .visual {
+            flex: 0 0 auto;
+            display: flex;
+            align-items: center;
+            margin-left: 15px;
+
+            .fa {
+              display: block;
+              color: $ui-primary-color;
+              font-size: 48px;
+            }
+          }
+
+          .text {
+            font-size: 16px;
+            line-height: 30px;
+            color: $ui-primary-color;
+
+            h6 {
+              font-size: inherit;
+              line-height: inherit;
+              margin-bottom: 0;
+            }
+          }
+        }
+      }
+    }
   }
 
-  h1 {
-    font-family: 'mastodon-font-display', sans-serif;
-    font-size: 26px;
+  .extended-description {
+    padding: 50px 0;
+    font-family: 'mastodon-font-sans-serif', sans-serif;
+    font-size: 16px;
+    font-weight: 400;
+    font-size: 16px;
     line-height: 30px;
-    margin-bottom: 0;
-    font-weight: 500;
-    color: $ui-secondary-color;
+    color: $ui-primary-color;
 
-    small {
-      font-family: 'mastodon-font-sans-serif', sans-serif;
-      display: block;
-      font-size: 18px;
-      font-weight: 400;
-      color: $ui-base-lighter-color;
+    a {
+      color: $ui-highlight-color;
+      text-decoration: underline;
     }
   }
 
@@ -676,8 +603,15 @@
       padding: 0 20px;
     }
 
-    .information-board .container {
-      padding-right: 20px;
+    .information-board {
+
+      .container {
+        padding-right: 20px;
+      }
+
+      .section {
+        text-align: center;
+      }
 
       .panel {
         position: static;
@@ -691,10 +625,6 @@
       }
     }
 
-    .information-board .section {
-      text-align: center;
-    }
-
     .header-wrapper .mascot {
       left: 20px;
     }
@@ -712,6 +642,7 @@
 
       &.compact .hero .heading {
         padding-bottom: 20px;
+        text-align: initial;
       }
     }
 
@@ -720,51 +651,41 @@
       display: block;
     }
 
-    .links {
-      padding-top: 15px;
-      background: darken($ui-base-color, 4%);
-    }
-
     .header {
 
-      .hero {
-        margin-top: 30px;
-        padding: 0;
+      .links {
+        padding-top: 15px;
+        background: darken($ui-base-color, 4%);
 
-        .heading {
-          padding: 0 20px 20px;
+        a {
+          padding: 12px 8px;
         }
-      }
-
-      .floats {
-        display: none;
-      }
 
-      .heading,
-      .nav {
-        text-align: center;
-      }
+        .nav {
+          display: flex;
+          flex-flow: row wrap;
+          justify-content: space-around;
+        }
 
-      .nav {
-        display: flex;
-        flex-flow: row wrap;
-        justify-content: space-around;
+        .brand img {
+          left: 0;
+          top: 0;
+        }
       }
 
-      .links a {
-        padding: 12px 8px;
-      }
+      .hero {
+        margin-top: 30px;
+        padding: 0;
 
-      .heading h1 {
-        padding: 30px 0;
-      }
+        .floats {
+          display: none;
+        }
 
-      .links .brand img {
-        left: 0;
-        top: 0;
-      }
+        .heading {
+          padding: 30px 20px;
+          text-align: center;
+        }
 
-      .hero {
         .simple_form,
         .closed-registrations-message {
           background: darken($ui-base-color, 8%);
@@ -775,7 +696,7 @@
       }
     }
 
-    #mastodon-timeline {
+    .features #mastodon-timeline {
       height: 70vh;
       width: 100%;
       margin-bottom: 50px;
diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss
index 5a9105109..744650554 100644
--- a/app/javascript/styles/accounts.scss
+++ b/app/javascript/styles/accounts.scss
@@ -1,34 +1,48 @@
 .card {
-  display: flex;
-  background: $ui-base-color;
+  background-color: lighten($ui-base-color, 4%);
   background-size: cover;
   background-position: center;
   border-radius: 4px 4px 0 0;
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
   overflow: hidden;
+  position: relative;
+  display: flex;
+
+  &::after {
+    background: rgba(darken($ui-base-color, 8%), 0.5);
+    display: block;
+    content: "";
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 1;
+  }
 
   @media screen and (max-width: 740px) {
     border-radius: 0;
     box-shadow: none;
   }
 
-  .details {
+  .card__illustration {
+    padding: 60px 0;
     position: relative;
-    padding: 60px 0 0;
-    text-align: center;
-    flex: auto;
+    flex: 1 1 auto;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
 
-    &::after {
-      background: linear-gradient(rgba($base-shadow-color, 0.5), rgba($base-shadow-color, 0.8));
-      display: block;
-      content: "";
-      position: absolute;
-      left: 0;
-      top: 0;
-      width: 100%;
-      height: 100%;
-      z-index: 1;
-    }
+  .card__bio {
+    max-width: 260px;
+    flex: 1 1 auto;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    background: rgba(darken($ui-base-color, 8%), 0.8);
+    position: relative;
+    z-index: 2;
   }
 
   &.compact {
@@ -46,14 +60,15 @@
 
   .name {
     display: block;
-    position: relative;
     font-size: 20px;
     line-height: 18px * 1.5;
     color: $primary-text-color;
+    padding: 10px 15px;
+    padding-bottom: 0;
     font-weight: 500;
-    text-align: center;
-    text-shadow: 0 0 2px $base-shadow-color;
+    position: relative;
     z-index: 2;
+    margin-bottom: 30px;
 
     small {
       display: block;
@@ -64,56 +79,102 @@
   }
 
   .avatar {
-    position: relative;
-    @include avatar-size(120px);
+    width: 120px;
     margin: 0 auto;
-    margin-bottom: 15px;
+    position: relative;
     z-index: 2;
 
     img {
-      @include avatar-radius();
-      @include avatar-size(120px);
+      width: 120px;
+      height: 120px;
       display: block;
+      border-radius: 120px;
+      box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
     }
   }
 
   .controls {
     position: absolute;
-    top: 10px;
-    right: 10px;
+    top: 15px;
+    left: 15px;
     z-index: 2;
+
+    .icon-button {
+      color: rgba($white, 0.8);
+      text-decoration: none;
+      font-size: 13px;
+      line-height: 13px;
+      font-weight: 500;
+
+      .fa {
+        font-weight: 400;
+        margin-right: 5px;
+      }
+
+      &:hover,
+      &:active,
+      &:focus {
+        color: $white;
+      }
+    }
+  }
+
+  .roles {
+    margin-bottom: 30px;
+    padding: 0 15px;
   }
 
   .details-counters {
-    display: inline-flex;
-    position: relative;
+    margin-top: 30px;
+    display: flex;
     flex-direction: row;
-    margin: 15px 0;
-    z-index: 2;
+    width: 100%;
   }
 
   .counter {
-    width: 80px;
+    width: 33.3%;
+    box-sizing: border-box;
+    flex: 0 0 auto;
     color: $ui-primary-color;
     padding: 5px 10px 0;
+    margin-bottom: 10px;
+    border-right: 1px solid lighten($ui-base-color, 4%);
     cursor: default;
+    text-align: center;
     position: relative;
 
-    & + .counter {
-      border-left: 1px solid $ui-primary-color;
+    a {
+      display: block;
+    }
+
+    &:last-child {
+      border-right: 0;
     }
 
-    & > * {
-      opacity: .7;
-      transition: opacity .3s ease;
+    &::after {
+      display: block;
+      content: "";
+      position: absolute;
+      bottom: -10px;
+      left: 0;
+      width: 100%;
+      border-bottom: 4px solid $ui-primary-color;
+      opacity: 0.5;
+      transition: all 400ms ease;
     }
 
-    &.active > *, &:hover > * {
-      opacity: 1;
+    &.active {
+      &::after {
+        border-bottom: 4px solid $ui-highlight-color;
+        opacity: 1;
+      }
     }
 
-    a {
-      display: block;
+    &:hover {
+      &::after {
+        opacity: 1;
+        transition-duration: 100ms;
+      }
     }
 
     a {
@@ -123,87 +184,40 @@
 
     .counter-label {
       font-size: 12px;
-      text-transform: uppercase;
       display: block;
       margin-bottom: 5px;
-      text-shadow: 0 0 2px $base-shadow-color;
     }
 
     .counter-number {
       font-weight: 500;
       font-size: 18px;
       color: $primary-text-color;
+      font-family: 'mastodon-font-display', sans-serif;
     }
   }
 
   .bio {
-    position: relative;
     font-size: 14px;
     line-height: 18px;
-    margin: 15px 0;
-    padding: 5px 10px;
+    padding: 0 15px;
     color: $ui-secondary-color;
-    z-index: 2;
-  }
-
-  .metadata {
-    position: relative;
-    min-width: 180px;
-    max-width: 40%;
-    background: rgba($base-shadow-color, 0.8);
-    color: $primary-text-color;
-    text-align: left;
-    overflow-y: auto;
-    white-space: pre-wrap;
-    z-index: 3;
-
-    .metadata-item {
-      border-bottom: 1px $ui-primary-color solid;
-      padding: 15px 10px;
-      font-size: 18px;
-      line-height: 24px;
-      overflow: hidden;
-      text-overflow: ellipsis;
-
-      a {
-        color: $ui-highlight-color;
-        text-decoration: none;
-
-        &:hover {
-          text-decoration: underline;
-        }
-      }
-
-      b {
-        display: block;
-        font-size: 12px;
-        line-height: 16px;
-        text-transform: uppercase;
-        color: $ui-primary-color;
-
-        a {
-          color: $ui-primary-color;
-        }
-      }
-    }
   }
-}
-
-
-
-@media screen and (max-width: 500px) {
 
-  .card {
+  @media screen and (max-width: 480px) {
     display: block;
 
-    .metadata {
+    .card__bio {
       max-width: none;
-      background: $base-shadow-color;
-      border-top: 1px $ui-primary-color solid;
+    }
 
-      .metadata-item {
-        padding: 15px 20px;
-      }
+    .name,
+    .roles {
+      text-align: center;
+      margin-bottom: 15px;
+    }
+
+    .bio {
+      margin-bottom: 15px;
     }
   }
 }
@@ -282,7 +296,9 @@
     }
 
     .next,
-    .prev {
+    .prev,
+    .next a,
+    .prev a {
       display: inline-block;
     }
   }
@@ -290,13 +306,15 @@
 
 .accounts-grid {
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-  background: $simple-background-color;
+  background: darken($simple-background-color, 8%);
   border-radius: 0 0 4px 4px;
-  padding: 20px 10px;
+  padding: 20px 5px;
   padding-bottom: 10px;
   overflow: hidden;
   display: flex;
   flex-wrap: wrap;
+  z-index: 2;
+  position: relative;
 
   @media screen and (max-width: 740px) {
     border-radius: 0;
@@ -306,35 +324,64 @@
   .account-grid-card {
     box-sizing: border-box;
     width: 335px;
-    border: 1px solid $ui-secondary-color;
+    background: $simple-background-color;
     border-radius: 4px;
     color: $ui-base-color;
-    margin-bottom: 10px;
+    margin: 0 5px 10px;
+    position: relative;
 
-    &:nth-child(odd) {
-      margin-right: 10px;
+    @media screen and (max-width: 740px) {
+      width: calc(100% - 10px);
     }
 
     .account-grid-card__header {
       overflow: hidden;
-      padding: 10px;
-      border-bottom: 1px solid $ui-secondary-color;
+      height: 100px;
+      border-radius: 4px 4px 0 0;
+      background-color: lighten($ui-base-color, 4%);
+      background-size: cover;
+      background-position: center;
+      position: relative;
+
+      &::after {
+        background: rgba(darken($ui-base-color, 8%), 0.5);
+        display: block;
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        z-index: 1;
+      }
+    }
+
+    .account-grid-card__avatar {
+      box-sizing: border-box;
+      padding: 15px;
+      position: absolute;
+      z-index: 2;
+      top: 100px - (40px + 2px);
+      left: -2px;
     }
 
     .avatar {
-      @include avatar-size(60px);
-      float: left;
-      margin-right: 15px;
+      width: 80px;
+      height: 80px;
 
       img {
-        @include avatar-radius();
-        @include avatar-size(60px);
         display: block;
+        width: 80px;
+        height: 80px;
+        border-radius: 80px;
+        border: 2px solid $simple-background-color;
       }
     }
 
     .name {
+      padding: 15px;
       padding-top: 10px;
+      padding-left: 15px + 80px + 15px;
 
       a {
         display: block;
@@ -342,6 +389,7 @@
         text-decoration: none;
         text-overflow: ellipsis;
         overflow: hidden;
+        font-weight: 500;
 
         &:hover {
           .display_name {
@@ -352,30 +400,38 @@
     }
 
     .display_name {
-      font-size: 14px;
+      font-size: 16px;
       display: block;
+      text-overflow: ellipsis;
+      overflow: hidden;
     }
 
     .username {
-      color: $ui-highlight-color;
+      color: lighten($ui-base-color, 34%);
+      font-size: 14px;
+      font-weight: 400;
     }
 
     .note {
-      padding: 10px;
+      padding: 10px 15px;
       padding-top: 15px;
-      color: $ui-primary-color;
+      box-sizing: border-box;
+      color: lighten($ui-base-color, 26%);
       word-wrap: break-word;
+      min-height: 80px;
     }
   }
 }
 
 .nothing-here {
+  width: 100%;
+  display: block;
   color: $ui-primary-color;
   font-size: 14px;
   font-weight: 500;
   text-align: center;
-  padding: 15px 0;
-  padding-bottom: 25px;
+  padding: 60px 0;
+  padding-top: 55px;
   cursor: default;
 }
 
@@ -396,14 +452,15 @@
     }
 
     & > div {
-      @include avatar-size(48px);
       float: left;
       margin-right: 10px;
+      width: 48px;
+      height: 48px;
     }
 
     .avatar {
-      @include avatar-radius();
       display: block;
+      border-radius: 4px;
     }
 
     .display-name {
@@ -439,3 +496,43 @@
     color: $ui-base-color;
   }
 }
+
+.activity-stream-tabs {
+  background: $simple-background-color;
+  border-bottom: 1px solid $ui-secondary-color;
+  position: relative;
+  z-index: 2;
+
+  a {
+    display: inline-block;
+    padding: 15px;
+    text-decoration: none;
+    color: $ui-highlight-color;
+    text-transform: uppercase;
+    font-weight: 500;
+
+    &:hover,
+    &:active,
+    &:focus {
+      color: lighten($ui-highlight-color, 8%);
+    }
+
+    &.active {
+      color: $ui-base-color;
+      cursor: default;
+    }
+  }
+}
+
+.account-role {
+  display: inline-block;
+  padding: 4px 6px;
+  cursor: default;
+  border-radius: 3px;
+  font-size: 12px;
+  line-height: 12px;
+  font-weight: 500;
+  color: $success-green;
+  background-color: rgba($success-green, 0.1);
+  border: 1px solid rgba($success-green, 0.5);
+}
diff --git a/app/javascript/styles/admin.scss b/app/javascript/styles/admin.scss
index 4c3bbdfc5..b86de75b6 100644
--- a/app/javascript/styles/admin.scss
+++ b/app/javascript/styles/admin.scss
@@ -32,7 +32,7 @@
 
       a {
         display: block;
-        padding: 15px 25px;
+        padding: 15px;
         color: rgba($primary-text-color, 0.7);
         text-decoration: none;
         transition: all 200ms linear;
@@ -61,6 +61,7 @@
 
         a {
           border: 0;
+          padding: 15px 35px;
 
           &.selected {
             color: $primary-text-color;
@@ -98,7 +99,7 @@
 
     h6 {
       font-size: 16px;
-      color: $ui-primary-color;
+      color: $ui-secondary-color;
       line-height: 28px;
       font-weight: 400;
     }
@@ -123,10 +124,10 @@
     }
 
     .muted-hint {
-      color: lighten($ui-base-color, 27%);
+      color: $ui-primary-color;
 
       a {
-        color: $ui-primary-color;
+        color: $ui-highlight-color;
       }
     }
 
@@ -139,15 +140,23 @@
   .simple_form {
     max-width: 400px;
 
-    .label_input {
-      label.select {
-        width: 50%;
-      }
+    &.edit_user,
+    &.new_form_admin_settings,
+    &.new_form_two_factor_confirmation,
+    &.new_form_delete_confirmation,
+    &.new_import,
+    &.new_domain_block,
+    &.edit_domain_block {
+      max-width: none;
+    }
 
-      select {
-        width: 50%;
-        float: right;
-      }
+    .form_two_factor_confirmation_code,
+    .form_delete_confirmation_password {
+      max-width: 400px;
+    }
+
+    .actions {
+      max-width: 400px;
     }
   }
 
@@ -227,27 +236,25 @@
 
 .report-accounts {
   display: flex;
+  flex-wrap: wrap;
   margin-bottom: 20px;
 }
 
 .report-accounts__item {
-  flex: 1 1 0;
   display: flex;
+  flex: 250px;
   flex-direction: column;
+  margin: 0 5px;
 
   & > strong {
     display: block;
-    margin-bottom: 10px;
+    margin: 0 0 10px -5px;
     font-weight: 500;
     font-size: 14px;
     line-height: 18px;
     color: $ui-secondary-color;
   }
 
-  &:first-child {
-    margin-right: 10px;
-  }
-
   .account-card {
     flex: 1 1 auto;
   }
@@ -261,6 +268,11 @@
   .activity-stream {
     flex: 2 0 0;
     margin-right: 20px;
+    max-width: calc(100% - 60px);
+
+    .entry {
+      border-radius: 4px;
+    }
   }
 }
 
@@ -280,18 +292,25 @@
 
 .batch-form-box {
   display: flex;
-  margin-bottom: 10px;
+  flex-wrap: wrap;
+  margin-bottom: 5px;
 
   #form_status_batch_action {
-    margin-right: 5px;
+    margin: 0 5px 5px 0;
     font-size: 14px;
   }
 
+  input.button {
+    margin: 0 5px 5px 0;
+  }
+
   .media-spoiler-toggle-buttons {
     margin-left: auto;
 
     .button {
       overflow: visible;
+      margin: 0 0 5px 5px;
+      float: right;
     }
   }
 }
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss
index 4e51b555c..05c93b42e 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/basics.scss
@@ -7,13 +7,28 @@ body {
   line-height: 18px;
   font-weight: 400;
   color: $primary-text-color;
-  padding-bottom: 140px;
+  padding-bottom: 20px;
   text-rendering: optimizelegibility;
   font-feature-settings: "kern";
   text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0,0,0,0);
   -webkit-tap-highlight-color: transparent;
 
+  &.system-font {
+    // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
+    // -apple-system => Safari <11 specific
+    // BlinkMacSystemFont => Chrome <56 on macOS specific
+    // Segoe UI => Windows 7/8/10
+    // Oxygen => KDE
+    // Ubuntu => Unity/Ubuntu
+    // Cantarell => GNOME
+    // Fira Sans => Firefox OS
+    // Droid Sans => Older Androids (<4.0)
+    // Helvetica Neue => Older macOS <10.11
+    // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", mastodon-font-sans-serif, sans-serif;
+  }
+
   &.app-body {
     position: fixed;
     width: 100%;
@@ -46,10 +61,6 @@ body {
     height: 100%;
     padding: 0;
   }
-
-  @media screen and (max-width: 400px) {
-    padding-bottom: 0;
-  }
 }
 
 button {
@@ -68,18 +79,3 @@ button {
   align-items: center;
   justify-content: center;
 }
-
-.system-font {
-  // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
-  // -apple-system => Safari <11 specific
-  // BlinkMacSystemFont => Chrome <56 on macOS specific
-  // Segoe UI => Windows 7/8/10
-  // Oxygen => KDE
-  // Ubuntu => Unity/Ubuntu
-  // Cantarell => GNOME
-  // Fira Sans => Firefox OS
-  // Droid Sans => Older Androids (<4.0)
-  // Helvetica Neue => Older macOS <10.11
-  // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
-  font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif;
-}
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 0d086ed45..2f2d6e1f0 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -238,6 +238,8 @@
   line-height: 0;
   display: inline-block;
   width: 0;
+  height: 0;
+  position: absolute;
 }
 
 .ellipsis {
@@ -395,6 +397,11 @@
       bottom: -1px;
       right: 8px;
     }
+
+    ::-webkit-scrollbar-track:hover,
+    ::-webkit-scrollbar-track:active {
+      background-color: rgba($base-overlay-background, 0.3);
+    }
   }
 }
 
@@ -1288,6 +1295,8 @@
   strong,
   span {
     display: block;
+    text-overflow: ellipsis;
+    overflow: hidden;
   }
 
   strong {
@@ -2130,7 +2139,7 @@
 
 .character-counter__wrapper {
   line-height: 36px;
-  margin-right: 16px;
+  margin: 0 16px 0 8px;
   padding-top: 10px;
 }
 
@@ -2313,6 +2322,18 @@ button.icon-button.active i.fa-retweet {
   background: lighten($ui-base-color, 8%);
 }
 
+.status-card.horizontal {
+  display: block;
+
+  .status-card__image {
+    width: 100%;
+  }
+
+  .status-card__image-image {
+    border-radius: 4px 4px 0 0;
+  }
+}
+
 .status-card__image-image {
   border-radius: 4px 0 0 4px;
   display: block;
@@ -2671,12 +2692,8 @@ button.icon-button.active i.fa-retweet {
 }
 
 .media-spoiler {
-  align-items: center;
   background: $base-overlay-background;
   color: $primary-text-color;
-  cursor: pointer;
-  display: flex;
-  flex-direction: column;
   border: 0;
   width: 100%;
   height: 100%;
@@ -3533,7 +3550,8 @@ button.icon-button.active i.fa-retweet {
 }
 
 .onboarding-modal,
-.error-modal {
+.error-modal,
+.embed-modal {
   background: $ui-secondary-color;
   color: $ui-base-color;
   border-radius: 8px;
@@ -4348,6 +4366,15 @@ noscript {
     margin: 30px auto;
     color: $ui-secondary-color;
     max-width: 400px;
+
+    a {
+      color: $ui-highlight-color;
+      text-decoration: underline;
+
+      &:hover {
+        text-decoration: none;
+      }
+    }
   }
 }
 
@@ -4455,3 +4482,61 @@ noscript {
     height: 100% !important;
   }
 }
+
+.embed-modal__html {
+  color: $ui-secondary-color;
+  outline: 0;
+  box-sizing: border-box;
+  display: block;
+  width: 100%;
+  border: none;
+  padding: 10px;
+  font-family: 'mastodon-font-monospace', monospace;
+  background: $ui-base-color;
+  color: $ui-primary-color;
+  font-size: 14px;
+  margin: 0;
+  margin-bottom: 15px;
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus,
+  &:active {
+    outline: 0 !important;
+  }
+
+  &:focus {
+    background: lighten($ui-base-color, 4%);
+  }
+
+  @media screen and (max-width: 600px) {
+    font-size: 16px;
+  }
+}
+
+.embed-modal {
+  h4 {
+    padding: 30px;
+    font-weight: 500;
+    font-size: 16px;
+    text-align: center;
+  }
+
+  .hint {
+    margin-bottom: 15px;
+  }
+}
+
+.embed-modal__container {
+  padding: 10px;
+}
+
+.embed-modal__iframe {
+  width: 100%;
+  min-width: 400px;
+  overflow: hidden;
+  border: 0;
+}
diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss
index 536f4e5a1..af2589e23 100644
--- a/app/javascript/styles/containers.scss
+++ b/app/javascript/styles/containers.scss
@@ -44,6 +44,22 @@
   }
 }
 
+.compose-standalone {
+  .compose-form {
+    width: 400px;
+    margin: 0 auto;
+    padding: 20px 0;
+    margin-top: 40px;
+    box-sizing: border-box;
+
+    @media screen and (max-width: 400px) {
+      width: 100%;
+      margin-top: 0;
+      padding: 20px;
+    }
+  }
+}
+
 .account-header {
   width: 400px;
   margin: 0 auto;
@@ -56,7 +72,7 @@
   margin-bottom: -30px;
   margin-top: 40px;
 
-  @media screen and (max-width: 400px) {
+  @media screen and (max-width: 440px) {
     width: 100%;
     margin: 0;
     margin-bottom: 10px;
@@ -81,10 +97,13 @@
   .name {
     flex: 1 1 auto;
     color: $ui-secondary-color;
+    width: calc(100% - 88px);
 
     .username {
       display: block;
       font-weight: 500;
+      text-overflow: ellipsis;
+      overflow: hidden;
     }
   }
 
@@ -92,5 +111,6 @@
     display: block;
     font-size: 32px;
     line-height: 40px;
+    margin-left: 8px;
   }
 }
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index 62094e98e..747610237 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -24,7 +24,7 @@ code {
 
   p.hint {
     margin-bottom: 15px;
-    color: lighten($ui-base-color, 32%);
+    color: $ui-primary-color;
 
     &.subtle-hint {
       text-align: center;
@@ -32,10 +32,10 @@ code {
       line-height: 18px;
       margin-top: 15px;
       margin-bottom: 0;
-      color: $ui-base-lighter-color;
+      color: $ui-primary-color;
 
       a {
-        color: $ui-primary-color;
+        color: $ui-highlight-color;
       }
     }
   }
@@ -53,7 +53,6 @@ code {
 
     label {
       flex: 0 0 auto;
-      width: 100px;
     }
 
     input {
@@ -65,12 +64,37 @@ code {
     padding: 15px 0;
     margin-bottom: 0;
 
+    .label_input {
+      flex-wrap: wrap;
+      align-items: flex-start;
+    }
+
+    &.select .label_input {
+      align-items: initial;
+    }
+
     .label_input > label {
       font-family: inherit;
       font-size: 16px;
       color: $primary-text-color;
       display: block;
       padding-top: 5px;
+      margin-bottom: 5px;
+      flex: 1;
+      min-width: 150px;
+      word-wrap: break-word;
+
+      &.select {
+        flex: 0;
+      }
+
+      & ~ * {
+        margin-left: 10px;
+      }
+    }
+
+    ul {
+      flex: 390px;
     }
 
     &.boolean {
@@ -359,17 +383,23 @@ code {
     color: $ui-secondary-color;
     font-weight: 500;
   }
+
+  @media screen and (max-width: 740px) and (min-width: 441px) {
+    margin-top: 40px;
+  }
 }
 
 .qr-wrapper {
   display: flex;
+  flex-wrap: wrap;
+  align-items: flex-start;
 }
 
 .qr-code {
   flex: 0 0 auto;
   background: $simple-background-color;
   padding: 4px;
-  margin-bottom: 20px;
+  margin: 0 10px 20px 0;
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
   display: inline-block;
 
@@ -380,8 +410,9 @@ code {
 }
 
 .qr-alternative {
-  margin-left: 10px;
-  color: $ui-primary-color;
+  margin-bottom: 20px;
+  color: $ui-secondary-color;
+  flex: 150px;
 
   samp {
     display: block;
@@ -391,7 +422,6 @@ code {
 
 .table-form {
   p {
-    max-width: 400px;
     margin-bottom: 15px;
 
     strong {
@@ -403,7 +433,6 @@ code {
 .simple_form,
 .table-form {
   .warning {
-    max-width: 400px;
     box-sizing: border-box;
     background: rgba($error-value-color, 0.5);
     color: $primary-text-color;
diff --git a/app/javascript/styles/landing_strip.scss b/app/javascript/styles/landing_strip.scss
index d2ac5b822..15ff84912 100644
--- a/app/javascript/styles/landing_strip.scss
+++ b/app/javascript/styles/landing_strip.scss
@@ -5,6 +5,8 @@
   padding: 14px;
   border-radius: 4px;
   margin-bottom: 20px;
+  display: flex;
+  align-items: center;
 
   strong,
   a {
@@ -15,4 +17,15 @@
     color: inherit;
     text-decoration: underline;
   }
+
+  .logo {
+    width: 30px;
+    height: 30px;
+    flex: 0 0 auto;
+    margin-right: 15px;
+  }
+
+  @media screen and (max-width: 740px) {
+    margin-bottom: 0;
+  }
 }
diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/rtl.scss
index 4966fbc21..6c003d69a 100644
--- a/app/javascript/styles/rtl.scss
+++ b/app/javascript/styles/rtl.scss
@@ -8,7 +8,7 @@ body.rtl {
   }
 
   .character-counter__wrapper {
-    margin-right: 0;
+    margin-right: 8px;
     margin-left: 16px;
   }
 
@@ -32,6 +32,11 @@ body.rtl {
     right: auto;
   }
 
+  .column-header__back-button {
+    padding-left: 5px;
+    padding-right: 0;
+  }
+
   .column-header__setting-arrows {
     float: left;
   }
@@ -54,25 +59,64 @@ body.rtl {
     right: 10px;
   }
 
-  .status {
+  .status,
+  .activity-stream .status.light {
     padding-left: 10px;
     padding-right: 68px;
   }
 
-  .status__info .status__display-name {
+  .status__info .status__display-name,
+  .activity-stream .status.light .status__display-name {
     padding-left: 25px;
     padding-right: 0;
   }
 
+  .activity-stream .pre-header {
+    padding-right: 68px;
+    padding-left: 0;
+  }
+
+  .status__prepend {
+    margin-left: 0;
+    margin-right: 68px;
+  }
+
+  .status__prepend-icon-wrapper {
+    left: auto;
+    right: -26px;
+  }
+
+  .activity-stream .pre-header .pre-header__icon {
+    left: auto;
+    right: 42px;
+  }
+
+  .account__avatar-overlay-overlay {
+    right: auto;
+    left: 0;
+  }
+
   .column-back-button--slim-button {
     right: auto;
     left: 0;
   }
 
-  .status__relative-time {
+  .status__relative-time,
+  .activity-stream .status.light .status__header .status__meta {
     float: left;
   }
 
+  .activity-stream .detailed-status.light .detailed-status__display-name > div {
+    float: right;
+    margin-right: 0;
+    margin-left: 10px;
+  }
+
+  .activity-stream .detailed-status.light .detailed-status__meta span > span {
+    margin-left: 0;
+    margin-right: 6px;
+  }
+
   .status__action-bar-button {
     float: right;
     margin-right: 0;
@@ -129,6 +173,78 @@ body.rtl {
     right: -2.14285714em;
   }
 
+  .admin-wrapper .sidebar ul a i.fa,
+  a.table-action-link i.fa {
+    margin-right: 0;
+    margin-left: 5px;
+  }
+
+  .simple_form .check_boxes .checkbox label,
+  .simple_form .input.with_label.boolean label.checkbox {
+    padding-left: 0;
+    padding-right: 25px;
+  }
+
+  .simple_form .check_boxes .checkbox input[type="checkbox"],
+  .simple_form .input.boolean input[type="checkbox"] {
+    left: auto;
+    right: 0;
+  }
+
+  .simple_form .input-with-append .input input {
+    padding-left: 127px;
+    padding-right: 0;
+  }
+
+  .simple_form .input-with-append .append {
+    right: auto;
+    left: 0;
+  }
+
+  .table th,
+  .table td {
+    text-align: right;
+  }
+
+  .filters .filter-subset {
+    margin-right: 0;
+    margin-left: 45px;
+  }
+
+  .landing-page .header-wrapper .mascot {
+    right: 60px;
+    left: auto;
+  }
+
+  .landing-page .header .hero .floats .float-1 {
+    left: -120px;
+    right: auto;
+  }
+
+  .landing-page .header .hero .floats .float-2 {
+    left: 210px;
+    right: auto;
+  }
+
+  .landing-page .header .hero .floats .float-3 {
+    left: 110px;
+    right: auto;
+  }
+
+  .landing-page .header .links .brand img {
+    left: 0;
+  }
+
+  .landing-page .fa-external-link {
+    padding-right: 5px;
+    padding-left: 0 !important;
+  }
+
+  .landing-page .features #mastodon-timeline {
+    margin-right: 0;
+    margin-left: 30px;
+  }
+
   @media screen and (min-width: 1025px) {
     .column,
     .drawer {
@@ -139,11 +255,6 @@ body.rtl {
         padding-left: 5px;
         padding-right: 10px;
       }
-
-      &:last-child {
-        padding-right: 0;
-        padding-left: 10px;
-      }
     }
 
     .columns-area > div {
diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss
index 90a43388b..00e430184 100644
--- a/app/javascript/styles/stream_entries.scss
+++ b/app/javascript/styles/stream_entries.scss
@@ -8,6 +8,7 @@
     .detailed-status.light,
     .status.light {
       border-bottom: 1px solid $ui-secondary-color;
+      animation: none;
     }
 
     &:last-child {
@@ -34,6 +35,14 @@
         }
       }
     }
+
+    @media screen and (max-width: 740px) {
+      &,
+      .detailed-status.light,
+      .status.light {
+        border-radius: 0 !important;
+      }
+    }
   }
 
   &.with-header {
@@ -44,6 +53,14 @@
         .status.light {
           border-radius: 0;
         }
+
+        &:last-child {
+          &,
+          .detailed-status.light,
+          .status.light {
+            border-radius: 0 0 4px 4px;
+          }
+        }
       }
     }
   }
@@ -400,3 +417,33 @@
     }
   }
 }
+
+.button.button-secondary.logo-button {
+  position: absolute;
+  right: 14px;
+  top: 14px;
+  font-size: 14px;
+
+  svg {
+    width: 20px;
+    height: auto;
+    vertical-align: middle;
+    margin-right: 5px;
+
+    path:first-child {
+      fill: $ui-primary-color;
+    }
+
+    path:last-child {
+      fill: $simple-background-color;
+    }
+  }
+
+  &:active,
+  &:focus,
+  &:hover {
+    svg path:first-child {
+      fill: lighten($ui-primary-color, 4%);
+    }
+  }
+}
diff --git a/app/javascript/styles/tables.scss b/app/javascript/styles/tables.scss
index 6e54c59c0..f6e57e196 100644
--- a/app/javascript/styles/tables.scss
+++ b/app/javascript/styles/tables.scss
@@ -46,7 +46,7 @@
   &.inline-table {
     td,
     th {
-      padding: 8px 0;
+      padding: 8px 2px;
     }
 
     & > tbody > tr:nth-child(odd) > td,