diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index c5efd46aa209d8286e1d49f34caacbcb5ef1fb53..c7afd4580f1bc65139033f49ffa553b78a44bc69 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -40,7 +40,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
       any: params[:any],
       all: params[:all],
       none: params[:none],
-      local: false,
+      local: truthy_param?(:local),
       remote: truthy_param?(:remote),
       only_media: truthy_param?(:only_media)
     )
@@ -51,7 +51,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
   end
 
   def pagination_params(core_params)
-    params.slice(:limit, :only_media).permit(:limit, :only_media).merge(core_params)
+    params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
   end
 
   def next_path
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index f75cd5ff4a546a5b7e91308e9f7d7d734d19e23c..6616ba107c83f3d4836582b68ecf3940ac1d4e70 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -43,7 +43,7 @@ class TagsController < ApplicationController
   end
 
   def set_local
-    @local = false
+    @local = truthy_param?(:local)
   end
 
   def set_statuses
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index e8983df867b6fe76c4a72de3f88822341b4a0098..7a5a719bdbb86795ceb387c2ef05dabea52506c9 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -198,6 +198,7 @@ export function submitCompose(routerHistory) {
       }
 
       if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+        insertIfOnline('community');
         insertIfOnline('public');
         insertIfOnline(`account:${response.data.account.id}`);
       }
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 10f5be5e8dbb3b83924034a50e96af77f335ce6e..b8e07d5dc42521c622ed6f09557b750f853ef18c 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -117,6 +117,14 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
 export const connectUserStream = () =>
   connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification });
 
+/**
+ * @param {Object} options
+ * @param {boolean} [options.onlyMedia]
+ * @return {function(): void}
+ */
+export const connectCommunityStream = ({ onlyMedia } = {}) =>
+  connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
+
 /**
  * @param {string} domain
  * @param {Object} options
@@ -148,11 +156,12 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
 /**
  * @param {string} columnId
  * @param {string} tagName
+ * @param {boolean} onlyLocal
  * @param {function(object): boolean} accept
  * @return {function(): void}
  */
-export const connectHashtagStream = (columnId, tagName, accept) =>
-  connectTimelineStream(`hashtag:${columnId}`, `hashtag`, { tag: tagName }, { accept });
+export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) =>
+  connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept });
 
 /**
  * @return {function(): void}
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 1e678a52778335e348db6d3326f2847ad3023bb6..376987bad66349253925dbbd3daee99a21ee35aa 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -121,18 +121,20 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 
 export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
 export const expandPublicTimeline          = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
 export const expandDomainTimeline          = (domain, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`domain${onlyMedia ? ':media' : ''}:${domain}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia }, done);
 export const expandGroupTimeline           = (id, { maxId, onlyMedia, tagged } = {}, done = noOp) => expandTimeline(`group:${id}${onlyMedia ? ':media' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: !!onlyMedia, tagged: tagged }, done);
 export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
 export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
 export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
 export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
-export const expandHashtagTimeline         = (hashtag, { maxId, tags } = {}, done = noOp) => {
-  return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
+export const expandHashtagTimeline         = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
+  return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
     max_id: maxId,
     any:    parseTags(tags, 'any'),
     all:    parseTags(tags, 'all'),
     none:   parseTags(tags, 'none'),
+    local:  local,
   }, done);
 };
 
diff --git a/app/javascript/mastodon/features/community_timeline/components/column_settings.js b/app/javascript/mastodon/features/community_timeline/components/column_settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..0cb6db8836b9bda09fcd78e3f89c80b789170322
--- /dev/null
+++ b/app/javascript/mastodon/features/community_timeline/components/column_settings.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export default @injectIntl
+class ColumnSettings extends React.PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+  };
+
+  render () {
+    const { settings, onChange } = this.props;
+
+    return (
+      <div>
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/community_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/community_timeline/containers/column_settings_container.js
new file mode 100644
index 0000000000000000000000000000000000000000..405064c3fcba30c1b26fb2c923d8be035f6dfcf7
--- /dev/null
+++ b/app/javascript/mastodon/features/community_timeline/containers/column_settings_container.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import ColumnSettings from '../components/column_settings';
+import { changeSetting } from '../../../actions/settings';
+import { changeColumnParams } from '../../../actions/columns';
+
+const mapStateToProps = (state, { columnId }) => {
+  const uuid = columnId;
+  const columns = state.getIn(['settings', 'columns']);
+  const index = columns.findIndex(c => c.get('uuid') === uuid);
+
+  return {
+    settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => {
+  return {
+    onChange (key, checked) {
+      if (columnId) {
+        dispatch(changeColumnParams(columnId, key, checked));
+      } else {
+        dispatch(changeSetting(['community', ...key], checked));
+      }
+    },
+  };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3cd39685cd79080b2994011bf93b9b84c6a811d
--- /dev/null
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -0,0 +1,137 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import { expandCommunityTimeline } from '../../actions/timelines';
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import ColumnSettingsContainer from './containers/column_settings_container';
+import { connectCommunityStream } from '../../actions/streaming';
+
+const messages = defineMessages({
+  title: { id: 'column.community', defaultMessage: 'Local timeline' },
+});
+
+const mapStateToProps = (state, { columnId }) => {
+  const uuid = columnId;
+  const columns = state.getIn(['settings', 'columns']);
+  const index = columns.findIndex(c => c.get('uuid') === uuid);
+  const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']);
+  const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]);
+
+  return {
+    hasUnread: !!timelineState && timelineState.get('unread') > 0,
+    onlyMedia,
+  };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class CommunityTimeline extends React.PureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
+  static defaultProps = {
+    onlyMedia: false,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    shouldUpdateScroll: PropTypes.func,
+    columnId: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    hasUnread: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    onlyMedia: PropTypes.bool,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch, onlyMedia } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('COMMUNITY', { other: { onlyMedia } }));
+    }
+  }
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  }
+
+  componentDidMount () {
+    const { dispatch, onlyMedia } = this.props;
+
+    dispatch(expandCommunityTimeline({ onlyMedia }));
+    this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.onlyMedia !== this.props.onlyMedia) {
+      const { dispatch, onlyMedia } = this.props;
+
+      this.disconnect();
+      dispatch(expandCommunityTimeline({ onlyMedia }));
+      this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  }
+
+  handleLoadMore = maxId => {
+    const { dispatch, onlyMedia } = this.props;
+
+    dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
+  }
+
+  render () {
+    const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
+    const pinned = !!columnId;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon='users'
+          active={hasUnread}
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        >
+          <ColumnSettingsContainer columnId={columnId} />
+        </ColumnHeader>
+
+        <StatusListContainer
+          trackScroll={!pinned}
+          scrollKey={`community_timeline-${columnId}`}
+          timelineId={`community${onlyMedia ? ':media' : ''}`}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
+          shouldUpdateScroll={shouldUpdateScroll}
+          bindToDocument={!multiColumn}
+        />
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 09f63143649229878629110548bc35d716d4c46e..5953d9647db045b332c2b3bd5127fa4fe04764f7 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -23,7 +23,7 @@ const messages = defineMessages({
   home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
-  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+  community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
   logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
   compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
@@ -104,6 +104,9 @@ class Compose extends React.PureComponent {
           {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
             <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
           )}
+          {!columns.some(column => column.get('id') === 'COMMUNITY') && (
+            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
+          )}
           {!columns.some(column => column.get('id') === 'PUBLIC') && (
             <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
           )}
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 1f07e554b0b0042b9bbe095ca4d20de7b3d6e873..b0968bc102275cec9025fe1c0a7d28062f7dcee7 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -20,6 +20,7 @@ const messages = defineMessages({
   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
+  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
   bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
@@ -102,10 +103,11 @@ class GettingStarted extends ImmutablePureComponent {
     if (multiColumn) {
       navItems.push(
         <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
+        <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
         <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
       );
 
-      height += 34 + 48;
+      height += 34 + 48*2;
 
       navItems.push(
         <ColumnLink key='group_directory' icon='address-book' text={intl.formatMessage(messages.group_directory)} to='/group_directory' />,
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
index 292c5cc6f5dc46043a33863de7643de11efeb0f0..27300f020df3ba54a5a842bfef5c6c02b0d679e6 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
@@ -5,6 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
 import AsyncSelect from 'react-select/async';
 import { NonceProvider } from 'react-select';
+import SettingToggle from '../../notifications/components/setting_toggle';
 
 const messages = defineMessages({
   placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
@@ -111,6 +112,10 @@ class ColumnSettings extends React.PureComponent {
             {this.modeSelect('none')}
           </div>
         )}
+
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
+        </div>
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js
index 9cc34b4e870b36581a73691d38c96218c22ccb41..5ccd9f8ea2ecd3c6a89dd6b0b8c3473d84a81906 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.js
@@ -12,7 +12,7 @@ import { connectHashtagStream } from '../../actions/streaming';
 import { isEqual } from 'lodash';
 
 const mapStateToProps = (state, props) => ({
-  hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
+  hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
 });
 
 export default @connect(mapStateToProps)
@@ -76,13 +76,13 @@ class HashtagTimeline extends React.PureComponent {
     this.column.scrollTop();
   }
 
-  _subscribe (dispatch, id, tags = {}) {
+  _subscribe (dispatch, id, tags = {}, local) {
     let any  = (tags.any || []).map(tag => tag.value);
     let all  = (tags.all || []).map(tag => tag.value);
     let none = (tags.none || []).map(tag => tag.value);
 
     [id, ...any].map(tag => {
-      this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
+      this.disconnects.push(dispatch(connectHashtagStream(id, tag, local, status => {
         let tags = status.tags.map(tag => tag.name);
 
         return all.filter(tag => tags.includes(tag)).length === all.length &&
@@ -98,21 +98,21 @@ class HashtagTimeline extends React.PureComponent {
 
   componentDidMount () {
     const { dispatch } = this.props;
-    const { id, tags } = this.props.params;
+    const { id, tags, local } = this.props.params;
 
-    this._subscribe(dispatch, id, tags);
-    dispatch(expandHashtagTimeline(id, { tags }));
+    this._subscribe(dispatch, id, tags, local);
+    dispatch(expandHashtagTimeline(id, { tags, local }));
   }
 
   componentWillReceiveProps (nextProps) {
     const { dispatch, params } = this.props;
-    const { id, tags } = nextProps.params;
+    const { id, tags, local } = nextProps.params;
 
-    if (id !== params.id || !isEqual(tags, params.tags)) {
+    if (id !== params.id || !isEqual(tags, params.tags) || !isEqual(local, params.local)) {
       this._unsubscribe();
-      this._subscribe(dispatch, id, tags);
-      dispatch(clearTimeline(`hashtag:${id}`));
-      dispatch(expandHashtagTimeline(id, { tags }));
+      this._subscribe(dispatch, id, tags, local);
+      dispatch(clearTimeline(`hashtag:${id}${local ? ':local' : ''}`));
+      dispatch(expandHashtagTimeline(id, { tags, local }));
     }
   }
 
@@ -125,13 +125,13 @@ class HashtagTimeline extends React.PureComponent {
   }
 
   handleLoadMore = maxId => {
-    const { id, tags } = this.props.params;
-    this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
+    const { id, tags, local } = this.props.params;
+    this.props.dispatch(expandHashtagTimeline(id, { maxId, tags, local }));
   }
 
   render () {
     const { shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
-    const { id } = this.props.params;
+    const { id, local } = this.props.params;
     const pinned = !!columnId;
 
     return (
@@ -153,7 +153,7 @@ class HashtagTimeline extends React.PureComponent {
         <StatusListContainer
           trackScroll={!pinned}
           scrollKey={`hashtag_timeline-${columnId}`}
-          timelineId={`hashtag:${id}`}
+          timelineId={`hashtag:${id}${local ? ':local' : ''}`}
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
           shouldUpdateScroll={shouldUpdateScroll}
diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js
index 23b7adeb9b49680ef7be37d4c42ea479800e1796..5820750a4ee0bd6427719f9fb316eecb42c9e19f 100644
--- a/app/javascript/mastodon/features/introduction/index.js
+++ b/app/javascript/mastodon/features/introduction/index.js
@@ -45,6 +45,11 @@ const FrameFederation = ({ onNext }) => (
         <p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p>
       </div>
 
+      <div>
+        <h3><FormattedMessage id='introduction.federation.local.headline' defaultMessage='Local' /></h3>
+        <p><FormattedMessage id='introduction.federation.local.text' defaultMessage='Public posts from people on the same server as you will appear in the local timeline.' /></p>
+      </div>
+
       <div>
         <h3><FormattedMessage id='introduction.federation.federated.headline' defaultMessage='Federated' /></h3>
         <p><FormattedMessage id='introduction.federation.federated.text' defaultMessage='Public posts from other servers of the fediverse will appear in the federated timeline.' /></p>
diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
index 03b599dc0af772ffc8261a3487600bfc2805fa4d..d278d2b264b7d3ceb41c3e9eddd30a5f69162613 100644
--- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js
+++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
@@ -112,6 +112,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
                 <td><kbd>g</kbd>+<kbd>n</kbd></td>
                 <td><FormattedMessage id='keyboard_shortcuts.notifications' defaultMessage='to open notifications column' /></td>
               </tr>
+              <tr>
+                <td><kbd>g</kbd>+<kbd>l</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.local' defaultMessage='to open local timeline' /></td>
+              </tr>
               <tr>
                 <td><kbd>g</kbd>+<kbd>t</kbd></td>
                 <td><FormattedMessage id='keyboard_shortcuts.federated' defaultMessage='to open federated timeline' /></td>
diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
index b8cd5b6a403e66f5f82491cc08724a94adcfa841..d3d8a6507e6d19701386a3e20b4dacae89f603dc 100644
--- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
@@ -24,20 +24,25 @@ class HashtagTimeline extends React.PureComponent {
     isLoading: PropTypes.bool.isRequired,
     hasMore: PropTypes.bool.isRequired,
     hashtag: PropTypes.string.isRequired,
+    local: PropTypes.bool.isRequired,
+  };
+
+  static defaultProps = {
+    local: false,
   };
 
   componentDidMount () {
-    const { dispatch, hashtag } = this.props;
+    const { dispatch, hashtag, local } = this.props;
 
-    dispatch(expandHashtagTimeline(hashtag));
+    dispatch(expandHashtagTimeline(hashtag, { local }));
   }
 
   handleLoadMore = () => {
-    const { dispatch, hashtag, statusIds } = this.props;
+    const { dispatch, hashtag, local, statusIds } = this.props;
     const maxId = statusIds.last();
 
     if (maxId) {
-      dispatch(expandHashtagTimeline(hashtag, { maxId }));
+      dispatch(expandHashtagTimeline(hashtag, { maxId, local }));
     }
   }
 
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
index 78f08ce3aa36ae77a217ad35be3cbceb252024e9..19b0b14be6a35d2decfb9f4f89825672b397f222 100644
--- a/app/javascript/mastodon/features/standalone/public_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -2,15 +2,15 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { expandPublicTimeline } from 'mastodon/actions/timelines';
+import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
 import Masonry from 'react-masonry-infinite';
 import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
 import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
 import { debounce } from 'lodash';
 import LoadingIndicator from 'mastodon/components/loading_indicator';
 
-const mapStateToProps = (state) => {
-  const timeline = state.getIn(['timelines', 'public'], ImmutableMap());
+const mapStateToProps = (state, { local }) => {
+  const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
 
   return {
     statusIds: timeline.get('items', ImmutableList()),
@@ -27,6 +27,7 @@ class PublicTimeline extends React.PureComponent {
     statusIds: ImmutablePropTypes.list.isRequired,
     isLoading: PropTypes.bool.isRequired,
     hasMore: PropTypes.bool.isRequired,
+    local: PropTypes.bool,
   };
 
   componentDidMount () {
@@ -34,20 +35,23 @@ class PublicTimeline extends React.PureComponent {
   }
 
   componentDidUpdate (prevProps) {
+    if (prevProps.local !== this.props.local) {
+      this._connect();
+    }
   }
 
   _connect () {
-    const { dispatch } = this.props;
+    const { dispatch, local } = this.props;
 
-    dispatch(expandPublicTimeline());
+    dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
   }
 
   handleLoadMore = () => {
-    const { dispatch, statusIds } = this.props;
+    const { dispatch, statusIds, local } = this.props;
     const maxId = statusIds.last();
 
     if (maxId) {
-      dispatch(expandPublicTimeline({ maxId }));
+      dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 08ef1d911cb33a8fa5e284d4baa80640617e4a67..f2f71362e52a6e2314f2a3724aa7009ae9aac7b2 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -19,6 +19,7 @@ import {
   Notifications,
   HomeTimeline,
   GroupTimeline,
+  CommunityTimeline,
   PublicTimeline,
   DomainTimeline,
   HashtagTimeline,
@@ -43,6 +44,7 @@ const componentMap = {
   'NOTIFICATIONS': Notifications,
   'PUBLIC': PublicTimeline,
   'REMOTE': PublicTimeline,
+  'COMMUNITY': CommunityTimeline,
   'DOMAIN': DomainTimeline,
   'GROUP': GroupTimeline,
   'HASHTAG': HashtagTimeline,
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index 1f0c2cf27101d9fa41e363f81138668bccad6dcc..3df724bb48d6ca77ba52a7f68c95e765ede6eb7f 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -15,6 +15,7 @@ const NavigationPanel = () => (
     <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
     <FollowRequestsNavLink />
+    <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
     <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/accounts/95378'><Icon className='column-link__icon' id='info-circle' fixedWidth /><FormattedMessage id='navigation_bar.information_acct' defaultMessage='QOTO info' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/timelines/tag/QOTOJournal'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='navigation_bar.hashtag_qoto_journal' defaultMessage='QOTO Journal' /></NavLink>
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index e5ad19949963b957e2381c1a04e523016a362dce..1e53dd8aa0faa46e5b72790dd15627fd72824d2c 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -10,6 +10,7 @@ import NotificationsCounterIcon from './notifications_counter_icon';
 export const links = [
   <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
   <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
   <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
   <NavLink className='tabs-bar__link' exact to='/lists' data-preview-title-id='column.lists' data-preview-icon='list-ul' ><Icon id='list-ul' fixedWidth /><FormattedMessage id='tabs_bar.lists' defaultMessage='Lists' /></NavLink>,
   <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 3053f78c497f3d391fa38deb1e3d60fff8ed35de..4fc2ea78e27dc6a316224ab86df80e83bcf5c2fb 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -28,6 +28,7 @@ import {
   GettingStarted,
   KeyboardShortcuts,
   PublicTimeline,
+  CommunityTimeline,
   DomainTimeline,
   GroupTimeline,
   AccountTimeline,
@@ -95,6 +96,7 @@ const keyMap = {
   back: 'backspace',
   goToHome: 'g h',
   goToNotifications: 'g n',
+  goToLocal: 'g l',
   goToFederated: 'g t',
   goToDirect: 'g d',
   goToStart: 'g s',
@@ -191,6 +193,7 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
           <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
+          <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/public/domain/:domain' exact component={DomainTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/groups/:id/:tagged?' exact component={GroupTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
@@ -480,6 +483,10 @@ class UI extends React.PureComponent {
     this.context.router.history.push('/notifications');
   }
 
+  handleHotkeyGoToLocal = () => {
+    this.context.router.history.push('/timelines/public/local');
+  }
+
   handleHotkeyGoToFederated = () => {
     this.context.router.history.push('/timelines/public');
   }
@@ -530,6 +537,7 @@ class UI extends React.PureComponent {
       back: this.handleHotkeyBack,
       goToHome: this.handleHotkeyGoToHome,
       goToNotifications: this.handleHotkeyGoToNotifications,
+      goToLocal: this.handleHotkeyGoToLocal,
       goToFederated: this.handleHotkeyGoToFederated,
       goToDirect: this.handleHotkeyGoToDirect,
       goToStart: this.handleHotkeyGoToStart,
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 1732c323159f199025a7f7eb3ae033dd4f2a7bb4..4df47fdbc6a3cf6b5d3000e21597bec5074366b9 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -18,6 +18,10 @@ export function PublicTimeline () {
   return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
 }
 
+export function CommunityTimeline () {
+  return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
+}
+
 export function DomainTimeline () {
   return import(/* webpackChunkName: "features/domain_timeline" */'../../domain_timeline');
 }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index e49837fc8aa3ce9cda6638e5685a8e808b80b76e..2c83357aa8fef2b6313d520a42cde46806be9678 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -245,7 +245,7 @@
   "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
   "introduction.federation.action": "Next",
   "introduction.federation.federated.headline": "Federated",
-  "introduction.federation.federated.text": "Public posts from various Fediverse servers are displayed on the federation timeline.",
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
   "introduction.federation.home.headline": "Home",
   "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
   "introduction.federation.local.headline": "Local",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 2488cd9788ff8e79c1087f0b700443f6f6b9b384..e95fbac1b1bba5feb1bd792f2d80c38da8acf572 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -241,7 +241,7 @@
   "intervals.full.minutes": "{number}分",
   "introduction.federation.action": "次へ",
   "introduction.federation.federated.headline": "連合タイムライン",
-  "introduction.federation.federated.text": "Fediverseの様々なサーバーからの公開投稿が連合タイムラインに表示されます。",
+  "introduction.federation.federated.text": "Fediverseの他のサーバーからの公開投稿は連合タイムラインに表示されます。",
   "introduction.federation.home.headline": "ホームタイムライン",
   "introduction.federation.home.text": "フォローしている人々の投稿はホームタイムラインに表示されます。どこのサーバーの誰でもフォローできます!",
   "introduction.federation.local.headline": "ローカルタイムライン",
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 7abc4064eb111b59c497641adf9b0bde97394f8e..c8c7ba39055baa22539a3ba6f3cab165a1071c18 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -65,6 +65,12 @@ const initialState = ImmutableMap({
     }),
   }),
 
+  community: ImmutableMap({
+    regex: ImmutableMap({
+      body: '',
+    }),
+  }),
+
   domain: ImmutableMap({
     regex: ImmutableMap({
       body: '',
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 2d92e73d6e4fe904d648dfb4486baba4049bafc3..19f63451eb85eac409a571ba47d044afe66c2904 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -99,7 +99,7 @@ const sharedCallbacks = {
 
       const streamIdentifier = stream[1];
 
-      if (channelName === 'hashtag') {
+      if (['hashtag', 'hashtag:local'].includes(channelName)) {
         return channelName === streamChannelName && params.tag === streamIdentifier;
       } else if (channelName === 'list') {
         return channelName === streamChannelName && params.list === streamIdentifier;
diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb
index 403ee071c9014ca92af96d0e960c290b8ba2e9d7..c6d0b37a93e1a30e56c48e88a04bbb827be460a9 100644
--- a/app/models/public_feed.rb
+++ b/app/models/public_feed.rb
@@ -19,13 +19,12 @@ class PublicFeed < Feed
   # @param [Integer] min_id
   # @return [Array<Status>]
   def get(limit, max_id = nil, since_id = nil, min_id = nil)
-    return Status.none if local_only? && !imast?
-
     scope = public_scope
 
     scope.merge!(without_replies_scope) unless with_replies?
     scope.merge!(without_reblogs_scope) unless with_reblogs?
     scope.merge!(remote_only_scope) if remote_only?
+    scope.merge!(local_only_scope) if local_only?
     scope.merge!(domain_only_scope) if domain_only?
     scope.merge!(account_filters_scope) if account?
     scope.merge!(media_only_scope) if media_only?
diff --git a/app/models/tag_feed.rb b/app/models/tag_feed.rb
index 0da6606de6e8b9a6abf43a017f34637993e07eda..9a16ffc82b312123a1effa1b56f7f87db3845672 100644
--- a/app/models/tag_feed.rb
+++ b/app/models/tag_feed.rb
@@ -29,6 +29,7 @@ class TagFeed < PublicFeed
     scope.merge!(tagged_with_any_scope)
     scope.merge!(tagged_with_all_scope)
     scope.merge!(tagged_with_none_scope)
+    scope.merge!(local_only_scope) if local_only?
     scope.merge!(remote_only_scope) if remote_only?
     scope.merge!(account_filters_scope) if account?
     scope.merge!(media_only_scope) if media_only?
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index cc1988f48b13c3c3362b2e84c85957cde9c2c6f8..406f25feab472095e8a270027ec9a46d4d687f9d 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -96,6 +96,7 @@ class BatchedRemoveStatusService < BaseService
     redis.pipelined do
       redis.publish('timeline:public', payload)
       if status.local?
+        redis.publish('timeline:public:local', payload)
       else
         redis.publish('timeline:public:remote', payload)
         redis.publish("timeline:public:domain:#{@domains[status.id].mb_chars.downcase}", payload)
@@ -104,6 +105,7 @@ class BatchedRemoveStatusService < BaseService
       if status.media_attachments.any?
         redis.publish('timeline:public:media', payload)
         if status.local?
+          redis.publish('timeline:public:local:media', payload)
         else
           redis.publish('timeline:public:remote:media', payload)
           redis.publish("timeline:public:domain:media:#{@domains[status.id].mb_chars.downcase}", payload)
@@ -112,6 +114,7 @@ class BatchedRemoveStatusService < BaseService
 
       @tags[status.id].each do |hashtag|
         redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload)
+        redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local?
       end
     end
   end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 332c2ed61185658945d6707ba2c240f78c3ad166..3e48e0440be133599a9613244e1f3ed766050cd2 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -179,6 +179,7 @@ class FanOutOnWriteService < BaseService
 
     status.tags.pluck(:name).each do |hashtag|
       Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
+      Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if status.local?
     end
   end
 
@@ -226,6 +227,7 @@ class FanOutOnWriteService < BaseService
 
     Redis.current.publish('timeline:public', @payload)
     if status.local?
+      Redis.current.publish('timeline:public:local', @payload)
     else
       Redis.current.publish('timeline:public:remote', @payload)
       Redis.current.publish("timeline:public:domain:#{status.account.domain.mb_chars.downcase}", @payload)
@@ -237,6 +239,7 @@ class FanOutOnWriteService < BaseService
 
     Redis.current.publish('timeline:public:media', @payload)
     if status.local?
+      Redis.current.publish('timeline:public:local:media', @payload)
     else
       Redis.current.publish('timeline:public:remote:media', @payload)
       Redis.current.publish("timeline:public:domain:media:#{status.account.domain.mb_chars.downcase}", @payload)
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 1982b442b23f2593cad3ad3a637b383f9c658cb5..bb67162ad7d1f11b06e05b6b4f841857a46cb0fa 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -133,6 +133,7 @@ class RemoveStatusService < BaseService
 
     @tags.each do |hashtag|
       redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
+      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
     end
   end
 
@@ -159,6 +160,7 @@ class RemoveStatusService < BaseService
 
     redis.publish('timeline:public', @payload)
     if @status.local?
+      redis.publish('timeline:public:local', @payload)
     else
       redis.publish('timeline:public:remote', @payload)
       redis.publish("timeline:public:domain:#{@account.domain.mb_chars.downcase}", @payload)
@@ -170,6 +172,7 @@ class RemoveStatusService < BaseService
 
     redis.publish('timeline:public:media', @payload)
     if @status.local?
+      redis.publish('timeline:public:local:media', @payload)
     else
       redis.publish('timeline:public:remote:media', @payload)
       redis.publish("timeline:public:domain:media:#{@account.domain.mb_chars.downcase}", @payload)
diff --git a/app/views/user_mailer/welcome.html.haml b/app/views/user_mailer/welcome.html.haml
index 6f5cfb6932644d95ff4594dcaaf8990a34ff62a1..1f75ff48ae453457002a0df887749e569d00ff66 100644
--- a/app/views/user_mailer/welcome.html.haml
+++ b/app/views/user_mailer/welcome.html.haml
@@ -138,5 +138,7 @@
                                   %span= t 'user_mailer.welcome.tip_mobile_webapp'
                                 %li
                                   %span= t 'user_mailer.welcome.tip_following'
+                                %li
+                                  %span= t 'user_mailer.welcome.tip_local_timeline', instance: @instance
                                 %li
                                   %span= t 'user_mailer.welcome.tip_federated_timeline'
diff --git a/app/views/user_mailer/welcome.text.erb b/app/views/user_mailer/welcome.text.erb
index 1bf7fbf450d2e83156365078be034ce5247a6d88..e310d7ca6f963a1e6e431b847defbe5fdb5d1c0a 100644
--- a/app/views/user_mailer/welcome.text.erb
+++ b/app/views/user_mailer/welcome.text.erb
@@ -25,4 +25,5 @@
 
 * <%= t 'user_mailer.welcome.tip_mobile_webapp' %>
 * <%= t 'user_mailer.welcome.tip_following' %>
+* <%= t 'user_mailer.welcome.tip_local_timeline', instance: @instance %>
 * <%= t 'user_mailer.welcome.tip_federated_timeline' %>
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 3d2e1749f692a7c44322a074f3eb2d17c1cd241b..756236739a85a59a036965937fb34dee8c10b742 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1392,14 +1392,14 @@ ja:
       edit_profile_step: アイコンやヘッダーの画像をアップロードしたり、表示名を変更したりして、自分のプロフィールをカスタマイズすることができます。また、誰かからの新規フォローを許可する前にその人の様子を見ておきたい場合、アカウントを承認制にすることもできます。
       explanation: 始めるにあたってのアドバイスです
       final_action: 始めましょう
-      final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開した投稿は連合タイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #QOTO ハッシュタグが便利かもしれません。'
+      final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
       full_handle: あなたの正式なユーザーID
       full_handle_hint: 別のサーバーの友達とフォローやメッセージをやり取りする際には、これを伝えることになります。
       review_preferences_action: 設定の変更
       review_preferences_step: 受け取りたいメールの種類や投稿のデフォルト公開範囲など、ユーザー設定を必ず済ませておきましょう。目が回らない自信があるなら、アニメーション GIF を自動再生する設定もご検討ください。
       subject: Mastodon へようこそ
-      tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じサーバーの人がフォローしている人や、リレーを経由して送られてくる投稿だけが含まれるので、それが全てではありません。
-      tip_following: 最初は、サーバーの管理者が指定したアカウントをフォローした状態になっています。もっと興味のある人たちを見つけるには、#QOTO ハッシュタグタイムラインと連合タイムラインを確認してみましょう。
+      tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じサーバーの人がフォローしている人だけが含まれるので、それが全てではありません。
+      tip_following: 最初は、サーバーの管理者をフォローした状態になっています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してみましょう。
       tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じサーバーにいる隣人のようなものです!
       tip_mobile_webapp: お使いのモバイル端末で、ブラウザから Mastodon をホーム画面に追加できますか? もし追加できる場合、プッシュ通知の受け取りなど、まるで「普通の」アプリのような機能が楽しめます!
       tips: 豆知識
diff --git a/streaming/index.js b/streaming/index.js
index e055296923cbcce58ea8c8954e95bf50a585bff1..f175794bdb919efa92c0d27659f3807ee4f4f114 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -374,6 +374,8 @@ const startWorker = (workerId) => {
       return onlyMedia ? 'public:domain:media' : 'public:domain';
     case '/api/v1/streaming/hashtag':
       return 'hashtag';
+    case '/api/v1/streaming/hashtag/local':
+      return 'hashtag:local';
     case '/api/v1/streaming/direct':
       return 'direct';
     case '/api/v1/streaming/list':
@@ -393,6 +395,7 @@ const startWorker = (workerId) => {
     'group',
     'group:media',
     'hashtag',
+    'hashtag:local',
   ];
 
   /**
@@ -765,6 +768,13 @@ const startWorker = (workerId) => {
         options: { needsFiltering: true, notificationOnly: false },
       });
 
+      break;
+    case 'public:local':
+      resolve({
+        channelIds: ['timeline:public:local'],
+        options: { needsFiltering: true, notificationOnly: false },
+      });
+
       break;
     case 'public:remote':
       resolve({
@@ -812,6 +822,13 @@ const startWorker = (workerId) => {
         options: { needsFiltering: true, notificationOnly: false },
       });
 
+      break;
+    case 'public:local:media':
+      resolve({
+        channelIds: ['timeline:public:local:media'],
+        options: { needsFiltering: true, notificationOnly: false },
+      });
+
       break;
     case 'public:remote:media':
       resolve({
@@ -859,6 +876,17 @@ const startWorker = (workerId) => {
         });
       }
 
+      break;
+    case 'hashtag:local':
+      if (!params.tag || params.tag.length === 0) {
+        reject('No tag for stream provided');
+      } else {
+        resolve({
+          channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}:local`],
+          options: { needsFiltering: true, notificationOnly: false },
+        });
+      }
+
       break;
     case 'list':
       authorizeListAccess(params.list, req).then(() => {
@@ -884,7 +912,7 @@ const startWorker = (workerId) => {
   const streamNameFromChannelName = (channelName, params) => {
     if (channelName === 'list') {
       return [channelName, params.list];
-    } else if (channelName === 'hashtag') {
+    } else if (['hashtag', 'hashtag:local'].includes(channelName)) {
       return [channelName, params.tag];
     } else if (['public:domain', 'public:domain:media'].includes(channelName)) {
       return [channelName, params.domain];