about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js84
-rw-r--r--app/javascript/flavours/glitch/actions/search.js2
-rw-r--r--app/javascript/flavours/glitch/components/hashtag.js34
-rw-r--r--app/javascript/flavours/glitch/components/status.js1
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js34
-rw-r--r--app/javascript/flavours/glitch/features/drawer/results/index.js11
-rw-r--r--app/javascript/flavours/glitch/features/getting_started_misc/index.js25
-rw-r--r--app/javascript/flavours/glitch/features/list_editor/components/account.js23
-rw-r--r--app/javascript/flavours/glitch/features/list_editor/components/search.js16
-rw-r--r--app/javascript/flavours/glitch/features/list_editor/containers/account_container.js24
-rw-r--r--app/javascript/flavours/glitch/features/list_editor/containers/search_container.js17
-rw-r--r--app/javascript/flavours/glitch/features/list_editor/index.js10
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js59
-rw-r--r--app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js24
-rw-r--r--app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js21
-rw-r--r--app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js78
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts.js4
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js57
-rw-r--r--app/javascript/flavours/glitch/reducers/search.js4
-rw-r--r--app/javascript/flavours/glitch/styles/components/search.scss84
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss20
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js4
-rw-r--r--app/javascript/flavours/glitch/util/numbers.js10
26 files changed, 550 insertions, 101 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index d5c4a02f9..d67ab112e 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -72,6 +72,17 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
 export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
 export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 
+export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
+export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
+export const PINNED_ACCOUNTS_FETCH_FAIL    = 'PINNED_ACCOUNTS_FETCH_FAIL';
+
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY  = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY';
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR  = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR';
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE';
+
+export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET';
+
+
 export function fetchAccount(id) {
   return (dispatch, getState) => {
     dispatch(fetchRelationships([id]));
@@ -733,3 +744,76 @@ export function unpinAccountFail(error) {
     error,
   };
 };
+
+export function fetchPinnedAccounts() {
+  return (dispatch, getState) => {
+    dispatch(fetchPinnedAccountsRequest());
+
+    api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } })
+      .then(({ data }) => dispatch(fetchPinnedAccountsSuccess(data)))
+      .catch(err => dispatch(fetchPinnedAccountsFail(err)));
+  };
+};
+
+export function fetchPinnedAccountsRequest() {
+  return {
+    type: PINNED_ACCOUNTS_FETCH_REQUEST,
+  };
+};
+
+export function fetchPinnedAccountsSuccess(accounts, next) {
+  return {
+    type: PINNED_ACCOUNTS_FETCH_SUCCESS,
+    accounts,
+    next,
+  };
+};
+
+export function fetchPinnedAccountsFail(error) {
+  return {
+    type: PINNED_ACCOUNTS_FETCH_FAIL,
+    error,
+  };
+};
+
+export function fetchPinnedAccountsSuggestions(q) {
+  return (dispatch, getState) => {
+    const params = {
+      q,
+      resolve: false,
+      limit: 4,
+      following: true,
+    };
+
+    api(getState).get('/api/v1/accounts/search', { params })
+      .then(({ data }) => dispatch(fetchPinnedAccountsSuggestionsReady(q, data)));
+  };
+};
+
+export function fetchPinnedAccountsSuggestionsReady(query, accounts) {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
+    query,
+    accounts,
+  };
+};
+
+export function clearPinnedAccountsSuggestions() {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
+  };
+};
+
+export function changePinnedAccountsSuggestions(value) {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
+    value,
+  }
+};
+
+export function resetPinnedAccountsEditor() {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_RESET,
+  };
+};
+
diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js
index 13885c600..ec65bdf28 100644
--- a/app/javascript/flavours/glitch/actions/search.js
+++ b/app/javascript/flavours/glitch/actions/search.js
@@ -32,7 +32,7 @@ export function submitSearch() {
 
     dispatch(fetchSearchRequest());
 
-    api(getState).get('/api/v1/search', {
+    api(getState).get('/api/v2/search', {
       params: {
         q: value,
         resolve: true,
diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js
new file mode 100644
index 000000000..88689cc6c
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/hashtag.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+import { Link } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { shortNumberFormat } from 'flavours/glitch/util/numbers';
+
+const Hashtag = ({ hashtag }) => (
+  <div className='trends__item'>
+    <div className='trends__item__name'>
+      <Link to={`/timelines/tag/${hashtag.get('name')}`}>
+        #<span>{hashtag.get('name')}</span>
+      </Link>
+
+      <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
+    </div>
+
+    <div className='trends__item__current'>
+      {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
+    </div>
+
+    <div className='trends__item__sparkline'>
+      <Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
+        <SparklinesCurve style={{ fill: 'none' }} />
+      </Sparklines>
+    </div>
+  </div>
+);
+
+Hashtag.propTypes = {
+  hashtag: ImmutablePropTypes.map.isRequired,
+};
+
+export default Hashtag;
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index da1f74e6d..a87721ef8 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -528,6 +528,7 @@ export default class Status extends ImmutablePureComponent {
               {...other}
               status={status}
               account={status.get('account')}
+              showReplyCount={settings.get('show_reply_count')}
             />
           ) : null}
           {notification ? (
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index cd9a2ac67..8a840030a 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -33,6 +33,16 @@ const messages = defineMessages({
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
 });
 
+const obfuscatedCount = count => {
+  if (count < 0) {
+    return 0;
+  } else if (count <= 1) {
+    return count;
+  } else {
+    return '1+';
+  }
+};
+
 @injectIntl
 export default class StatusActionBar extends ImmutablePureComponent {
 
@@ -56,6 +66,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onPin: PropTypes.func,
     onBookmark: PropTypes.func,
     withDismiss: PropTypes.bool,
+    showReplyCount: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -63,6 +74,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
   // evaluate to false. See react-immutable-pure-component for usage.
   updateOnProps = [
     'status',
+    'showReplyCount',
     'withDismiss',
   ]
 
@@ -134,7 +146,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   render () {
-    const { status, intl, withDismiss } = this.props;
+    const { status, intl, withDismiss, showReplyCount } = this.props;
 
     const mutingConversation = status.get('muted');
     const anonymousAccess    = !me;
@@ -188,9 +200,27 @@ export default class StatusActionBar extends ImmutablePureComponent {
       <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
     );
 
+    let replyButton = (
+      <IconButton
+        className='status__action-bar-button'
+        disabled={anonymousAccess}
+        title={replyTitle}
+        icon={replyIcon}
+        onClick={this.handleReplyClick}
+      />
+    );
+    if (showReplyCount) {
+      replyButton = (
+        <div className='status__action-bar__counter'>
+          {replyButton}
+          <span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span>
+        </div>
+      );
+    }
+
     return (
       <div className='status__action-bar'>
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
+        {replyButton}
         <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} 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/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js
index 23dc0e3cf..ac7a14ef4 100644
--- a/app/javascript/flavours/glitch/features/drawer/results/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/results/index.js
@@ -12,6 +12,7 @@ import { Link } from 'react-router-dom';
 //  Components.
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import StatusContainer from 'flavours/glitch/containers/status_container';
+import Hashtag from 'flavours/glitch/components/hashtag';
 
 //  Utils.
 import Motion from 'flavours/glitch/util/optional_motion';
@@ -98,15 +99,7 @@ export default function DrawerResults ({
             <section>
               <h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
 
-              {hashtags.map(
-                hashtag => (
-                  <Link
-                    className='hashtag'
-                    key={hashtag}
-                    to={`/timelines/tag/${hashtag}`}
-                  >#{hashtag}</Link>
-                )
-              )}
+              {hashtags.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
             </section>
           ) : null}
         </div>
diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.js b/app/javascript/flavours/glitch/features/getting_started_misc/index.js
index b67e6f97f..ee4452472 100644
--- a/app/javascript/flavours/glitch/features/getting_started_misc/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.js
@@ -21,6 +21,7 @@ const messages = defineMessages({
   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
   info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
   keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
+  featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' },
 });
 
 @connect()
@@ -33,27 +34,33 @@ export default class gettingStartedMisc extends ImmutablePureComponent {
   };
 
   openOnboardingModal = (e) => {
-    e.preventDefault();
     this.props.dispatch(openModal('ONBOARDING'));
   }
 
+  openFeaturedAccountsModal = (e) => {
+    this.props.dispatch(openModal('PINNED_ACCOUNTS_EDITOR'));
+  }
+
   render () {
     const { intl } = this.props;
 
+    let i = 1;
+
     return (
       <Column icon='ellipsis-h' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
 
         <div className='scrollable'>
           <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
-          <ColumnLink key='19' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
-          <ColumnLink key='20' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
-          <ColumnLink key='21' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
-          <ColumnLink key='22' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
-          <ColumnLink icon='minus-circle' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' />
-          <ColumnLink key='23' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />
-          <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
-          <ColumnLink icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} />
+          <ColumnLink key='{i++}' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
+          <ColumnLink key='{i++}' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
+          <ColumnLink key='{i++}' icon='users' text={intl.formatMessage(messages.featured_users)} onClick={this.openFeaturedAccountsModal} />
+          <ColumnLink key='{i++}' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
+          <ColumnLink key='{i++}' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
+          <ColumnLink key='{i++}' icon='minus-circle' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' />
+          <ColumnLink key='{i++}' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />
+          <ColumnLink key='{i++}' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
+          <ColumnLink key='{i++}' icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} />
         </div>
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/list_editor/components/account.js b/app/javascript/flavours/glitch/features/list_editor/components/account.js
index f48df759d..71a8b7673 100644
--- a/app/javascript/flavours/glitch/features/list_editor/components/account.js
+++ b/app/javascript/flavours/glitch/features/list_editor/components/account.js
@@ -1,38 +1,17 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { makeGetAccount } from 'flavours/glitch/selectors';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Avatar from 'flavours/glitch/components/avatar';
 import DisplayName from 'flavours/glitch/components/display_name';
 import IconButton from 'flavours/glitch/components/icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
-import { removeFromListEditor, addToListEditor } from 'flavours/glitch/actions/lists';
+import { defineMessages } from 'react-intl';
 
 const messages = defineMessages({
   remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
   add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
 });
 
-const makeMapStateToProps = () => {
-  const getAccount = makeGetAccount();
-
-  const mapStateToProps = (state, { accountId, added }) => ({
-    account: getAccount(state, accountId),
-    added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
-  });
-
-  return mapStateToProps;
-};
-
-const mapDispatchToProps = (dispatch, { accountId }) => ({
-  onRemove: () => dispatch(removeFromListEditor(accountId)),
-  onAdd: () => dispatch(addToListEditor(accountId)),
-});
-
-@connect(makeMapStateToProps, mapDispatchToProps)
-@injectIntl
 export default class Account extends ImmutablePureComponent {
 
   static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/list_editor/components/search.js b/app/javascript/flavours/glitch/features/list_editor/components/search.js
index 45c4d0f2e..280632652 100644
--- a/app/javascript/flavours/glitch/features/list_editor/components/search.js
+++ b/app/javascript/flavours/glitch/features/list_editor/components/search.js
@@ -1,26 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
-import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
+import { defineMessages } from 'react-intl';
 import classNames from 'classnames';
 
 const messages = defineMessages({
   search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
 });
 
-const mapStateToProps = state => ({
-  value: state.getIn(['listEditor', 'suggestions', 'value']),
-});
-
-const mapDispatchToProps = dispatch => ({
-  onSubmit: value => dispatch(fetchListSuggestions(value)),
-  onClear: () => dispatch(clearListSuggestions()),
-  onChange: value => dispatch(changeListSuggestions(value)),
-});
-
-@connect(mapStateToProps, mapDispatchToProps)
-@injectIntl
 export default class Search extends React.PureComponent {
 
   static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js
new file mode 100644
index 000000000..782eb42f3
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import { injectIntl } from 'react-intl';
+import { removeFromListEditor, addToListEditor } from 'flavours/glitch/actions/lists';
+import Account from '../components/account';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId, added }) => ({
+    account: getAccount(state, accountId),
+    added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+  onRemove: () => dispatch(removeFromListEditor(accountId)),
+  onAdd: () => dispatch(addToListEditor(accountId)),
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
diff --git a/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js
new file mode 100644
index 000000000..5af20efbd
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { injectIntl } from 'react-intl';
+import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
+import Search from '../components/search';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['listEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onSubmit: value => dispatch(fetchListSuggestions(value)),
+  onClear: () => dispatch(clearListSuggestions()),
+  onChange: value => dispatch(changeListSuggestions(value)),
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search));
diff --git a/app/javascript/flavours/glitch/features/list_editor/index.js b/app/javascript/flavours/glitch/features/list_editor/index.js
index e6df4755a..b3be3070a 100644
--- a/app/javascript/flavours/glitch/features/list_editor/index.js
+++ b/app/javascript/flavours/glitch/features/list_editor/index.js
@@ -5,8 +5,8 @@ import { connect } from 'react-redux';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { injectIntl } from 'react-intl';
 import { setupListEditor, clearListSuggestions, resetListEditor } from 'flavours/glitch/actions/lists';
-import Account from './components/account';
-import Search from './components/search';
+import AccountContainer from './containers/account_container';
+import SearchContainer from './containers/search_container';
 import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
@@ -56,11 +56,11 @@ export default class ListEditor extends ImmutablePureComponent {
       <div className='modal-root__modal list-editor'>
         <h4>{title}</h4>
 
-        <Search />
+        <SearchContainer />
 
         <div className='drawer__pager'>
           <div className='drawer__inner list-editor__accounts'>
-            {accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
+            {accountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} added />)}
           </div>
 
           {showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />}
@@ -68,7 +68,7 @@ export default class ListEditor extends ImmutablePureComponent {
           <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
             {({ x }) =>
               (<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
-                {searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
+                {searchAccountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} />)}
               </div>)
             }
           </Motion>
diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js
index d3b7b00b5..f88e23c47 100644
--- a/app/javascript/flavours/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -35,34 +35,45 @@ export default class LocalSettingsPage extends React.PureComponent {
         <h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
         <LocalSettingsPageItem
           settings={settings}
-          item={['layout']}
-          id='mastodon-settings--layout'
-          options={[
-            { value: 'auto', message: intl.formatMessage(messages.layout_auto) },
-            { value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
-            { value: 'single', message: intl.formatMessage(messages.layout_mobile) },
-          ]}
+          item={['show_reply_count']}
+          id='mastodon-settings--reply-count'
           onChange={onChange}
         >
-          <FormattedMessage id='settings.layout' defaultMessage='Layout:' />
-        </LocalSettingsPageItem>
-        <LocalSettingsPageItem
-          settings={settings}
-          item={['stretch']}
-          id='mastodon-settings--stretch'
-          onChange={onChange}
-        >
-          <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
-        </LocalSettingsPageItem>
-        <LocalSettingsPageItem
-          settings={settings}
-          item={['navbar_under']}
-          id='mastodon-settings--navbar_under'
-          onChange={onChange}
-        >
-          <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
+          <FormattedMessage id='settings.show_reply_counter' defaultMessage='Display an estimate of the reply count' />
         </LocalSettingsPageItem>
         <section>
+          <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['layout']}
+            id='mastodon-settings--layout'
+            options={[
+              { value: 'auto', message: intl.formatMessage(messages.layout_auto) },
+              { value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
+              { value: 'single', message: intl.formatMessage(messages.layout_mobile) },
+            ]}
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.layout' defaultMessage='Layout:' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['stretch']}
+            id='mastodon-settings--stretch'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['navbar_under']}
+            id='mastodon-settings--navbar_under'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
+          </LocalSettingsPageItem>
+        </section>
+        <section>
           <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2>
           <LocalSettingsPageItem
             settings={settings}
diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js
new file mode 100644
index 000000000..149d05c32
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import { injectIntl } from 'react-intl';
+import { pinAccount, unpinAccount } from 'flavours/glitch/actions/accounts';
+import Account from 'flavours/glitch/features/list_editor/components/account';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId, added }) => ({
+    account: getAccount(state, accountId),
+    added: typeof added === 'undefined' ? state.getIn(['pinnedAccountsEditor', 'accounts', 'items']).includes(accountId) : added,
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+  onRemove: () => dispatch(unpinAccount(accountId)),
+  onAdd: () => dispatch(pinAccount(accountId)),
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js
new file mode 100644
index 000000000..5a1efce0a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { injectIntl } from 'react-intl';
+import {
+  fetchPinnedAccountsSuggestions,
+  clearPinnedAccountsSuggestions,
+  changePinnedAccountsSuggestions
+} from '../../../actions/accounts';
+import Search from 'flavours/glitch/features/list_editor/components/search';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['pinnedAccountsEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onSubmit: value => dispatch(fetchPinnedAccountsSuggestions(value)),
+  onClear: () => dispatch(clearPinnedAccountsSuggestions()),
+  onChange: value => dispatch(changePinnedAccountsSuggestions(value)),
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search));
diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js
new file mode 100644
index 000000000..7484e458e
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { fetchPinnedAccounts, clearPinnedAccountsSuggestions, resetPinnedAccountsEditor } from 'flavours/glitch/actions/accounts';
+import AccountContainer from './containers/account_container';
+import SearchContainer from './containers/search_container';
+import Motion from 'flavours/glitch/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['pinnedAccountsEditor', 'accounts', 'items']),
+  searchAccountIds: state.getIn(['pinnedAccountsEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onInitialize: () => dispatch(fetchPinnedAccounts()),
+  onClear: () => dispatch(clearPinnedAccountsSuggestions()),
+  onReset: () => dispatch(resetPinnedAccountsEditor()),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class PinnedAccountsEditor extends ImmutablePureComponent {
+
+  static propTypes = {
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    onInitialize: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+    onReset: PropTypes.func.isRequired,
+    title: PropTypes.string.isRequired,
+    accountIds: ImmutablePropTypes.list.isRequired,
+    searchAccountIds: ImmutablePropTypes.list.isRequired,
+  };
+
+  componentDidMount () {
+    const { onInitialize } = this.props;
+    onInitialize();
+  }
+
+  componentWillUnmount () {
+    const { onReset } = this.props;
+    onReset();
+  }
+
+  render () {
+    const { accountIds, searchAccountIds, onClear } = this.props;
+    const showSearch = searchAccountIds.size > 0;
+
+    return (
+      <div className='modal-root__modal list-editor'>
+        <h4><FormattedMessage id='endorsed_accounts_editor.endorsed_accounts' defaultMessage='Featured accounts' /></h4>
+
+        <SearchContainer />
+
+        <div className='drawer__pager'>
+          <div className='drawer__inner list-editor__accounts'>
+            {accountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} added />)}
+          </div>
+
+          {showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />}
+
+          <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+            {({ x }) =>
+              (<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                {searchAccountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} />)}
+              </div>)
+            }
+          </Motion>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 23a7603d8..c9f54804a 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -19,6 +19,7 @@ import {
   SettingsModal,
   EmbedModal,
   ListEditor,
+  PinnedAccountsEditor,
 } from 'flavours/glitch/util/async-components';
 
 const MODAL_COMPONENTS = {
@@ -36,6 +37,7 @@ const MODAL_COMPONENTS = {
   'EMBED': EmbedModal,
   'LIST_EDITOR': ListEditor,
   'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
+  'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
 };
 
 export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index c38b3cc95..c2f016a87 100644
--- a/app/javascript/flavours/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -6,6 +6,8 @@ import {
   FOLLOWING_EXPAND_SUCCESS,
   FOLLOW_REQUESTS_FETCH_SUCCESS,
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
+  PINNED_ACCOUNTS_FETCH_SUCCESS,
+  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
 } from 'flavours/glitch/actions/accounts';
 import {
   BLOCKS_FETCH_SUCCESS,
@@ -141,6 +143,8 @@ export default function accounts(state = initialState, action) {
   case MUTES_EXPAND_SUCCESS:
   case LIST_ACCOUNTS_FETCH_SUCCESS:
   case LIST_EDITOR_SUGGESTIONS_READY:
+  case PINNED_ACCOUNTS_FETCH_SUCCESS:
+  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY:
     return action.accounts ? normalizeAccounts(state, action.accounts) : state;
   case NOTIFICATIONS_EXPAND_SUCCESS:
   case SEARCH_FETCH_SUCCESS:
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index 7b7bc2ca2..218a5ac8f 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -28,6 +28,7 @@ import custom_emojis from './custom_emojis';
 import lists from './lists';
 import listEditor from './list_editor';
 import filters from './filters';
+import pinnedAccountsEditor from './pinned_accounts_editor';
 
 const reducers = {
   dropdown_menu,
@@ -59,6 +60,7 @@ const reducers = {
   lists,
   listEditor,
   filters,
+  pinnedAccountsEditor,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index 73d034dbe..51032f345 100644
--- a/app/javascript/flavours/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -11,6 +11,7 @@ const initialState = ImmutableMap({
   navbar_under : false,
   side_arm  : 'none',
   side_arm_reply_mode : 'keep',
+  show_reply_count : false,
   collapsed : ImmutableMap({
     enabled     : true,
     auto        : ImmutableMap({
diff --git a/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js b/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js
new file mode 100644
index 000000000..267521bb8
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js
@@ -0,0 +1,57 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import {
+  PINNED_ACCOUNTS_EDITOR_RESET,
+  PINNED_ACCOUNTS_FETCH_REQUEST,
+  PINNED_ACCOUNTS_FETCH_SUCCESS,
+  PINNED_ACCOUNTS_FETCH_FAIL,
+  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
+  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
+  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
+  ACCOUNT_PIN_SUCCESS,
+  ACCOUNT_UNPIN_SUCCESS,
+} from '../actions/accounts';
+
+const initialState = ImmutableMap({
+  accounts: ImmutableMap({
+    items: ImmutableList(),
+    loaded: false,
+    isLoading: false,
+  }),
+
+  suggestions: ImmutableMap({
+    value: '',
+    items: ImmutableList(),
+  }),
+});
+
+export default function listEditorReducer(state = initialState, action) {
+  switch(action.type) {
+  case PINNED_ACCOUNTS_EDITOR_RESET:
+    return initialState;
+  case PINNED_ACCOUNTS_FETCH_REQUEST:
+    return state.setIn(['accounts', 'isLoading'], true);
+  case PINNED_ACCOUNTS_FETCH_FAIL:
+    return state.setIn(['accounts', 'isLoading'], false);
+  case PINNED_ACCOUNTS_FETCH_SUCCESS:
+    return state.update('accounts', accounts => accounts.withMutations(map => {
+      map.set('isLoading', false);
+      map.set('loaded', true);
+      map.set('items', ImmutableList(action.accounts.map(item => item.id)));
+    }));
+  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE:
+    return state.setIn(['suggestions', 'value'], action.value);
+  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY:
+    return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR:
+    return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+      map.set('items', ImmutableList());
+      map.set('value', '');
+    }));
+  case ACCOUNT_PIN_SUCCESS:
+    return state.updateIn(['accounts', 'items'], list => list.unshift(action.relationship.id));
+  case ACCOUNT_UNPIN_SUCCESS:
+    return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.relationship.id));
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/flavours/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js
index dc6be97e2..9a525bf47 100644
--- a/app/javascript/flavours/glitch/reducers/search.js
+++ b/app/javascript/flavours/glitch/reducers/search.js
@@ -9,7 +9,7 @@ import {
   COMPOSE_REPLY,
   COMPOSE_DIRECT,
 } from 'flavours/glitch/actions/compose';
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
 const initialState = ImmutableMap({
   value: '',
@@ -39,7 +39,7 @@ export default function search(state = initialState, action) {
     return state.set('results', ImmutableMap({
       accounts: ImmutableList(action.results.accounts.map(item => item.id)),
       statuses: ImmutableList(action.results.statuses.map(item => item.id)),
-      hashtags: ImmutableList(action.results.hashtags),
+      hashtags: fromJS(action.results.hashtags),
     })).set('submitted', true);
   default:
     return state;
diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss
index 91861ea19..f9e4b5883 100644
--- a/app/javascript/flavours/glitch/styles/components/search.scss
+++ b/app/javascript/flavours/glitch/styles/components/search.scss
@@ -90,16 +90,80 @@
   font-weight: 500;
 }
 
-.search-results__hashtag {
-  display: block;
-  padding: 10px;
-  color: $secondary-text-color;
-  text-decoration: none;
+.trends {
+  &__header {
+    color: $dark-text-color;
+    background: lighten($ui-base-color, 2%);
+    border-bottom: 1px solid darken($ui-base-color, 4%);
+    font-weight: 500;
+    padding: 15px;
+    font-size: 16px;
+    cursor: default;
 
-  &:hover,
-  &:active,
-  &:focus {
-    color: lighten($secondary-text-color, 4%);
-    text-decoration: underline;
+    .fa {
+      display: inline-block;
+      margin-right: 5px;
+    }
+  }
+
+  &__item {
+    display: flex;
+    align-items: center;
+    padding: 15px;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    &__name {
+      flex: 1 1 auto;
+      color: $dark-text-color;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+
+      strong {
+        font-weight: 500;
+      }
+
+      a {
+        color: $darker-text-color;
+        text-decoration: none;
+        font-size: 14px;
+        font-weight: 500;
+        display: block;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+
+        &:hover,
+        &:focus,
+        &:active {
+          span {
+            text-decoration: underline;
+          }
+        }
+      }
+    }
+
+    &__current {
+      flex: 0 0 auto;
+      width: 100px;
+      font-size: 24px;
+      line-height: 36px;
+      font-weight: 500;
+      text-align: center;
+      color: $secondary-text-color;
+    }
+
+    &__sparkline {
+      flex: 0 0 auto;
+      width: 50px;
+
+      path {
+        stroke: lighten($highlight-text-color, 6%) !important;
+      }
+    }
   }
 }
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index cd17bb4fa..fbc26ed2a 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -417,15 +417,31 @@
   align-items: center;
   display: flex;
   margin-top: 8px;
+
+  &__counter {
+    display: inline-flex;
+    margin-right: 11px;
+    align-items: center;
+
+    .status__action-bar-button {
+      margin-right: 4px;
+    }
+
+    &__label {
+      display: inline-block;
+      width: 14px;
+      font-size: 12px;
+      font-weight: 500;
+      color: $action-button-color;
+    }
+  }
 }
 
 .status__action-bar-button {
-  float: left;
   margin-right: 18px;
 }
 
 .status__action-bar-dropdown {
-  float: left;
   height: 23.15px;
   width: 23.15px;
 }
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index 3d6d3d1f4..557ce317e 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -38,6 +38,10 @@ export function ListEditor () {
   return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor');
 }
 
+export function PinnedAccountsEditor () {
+  return import(/* webpackChunkName: "flavours/glitch/async/pinned_accounts_editor" */'flavours/glitch/features/pinned_accounts_editor');
+}
+
 export function DirectTimeline() {
   return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'flavours/glitch/features/direct_timeline');
 }
diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js
new file mode 100644
index 000000000..fdd8269ae
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/numbers.js
@@ -0,0 +1,10 @@
+import React, { Fragment } from 'react';
+import { FormattedNumber } from 'react-intl';
+
+export const shortNumberFormat = number => {
+  if (number < 1000) {
+    return <FormattedNumber value={number} />;
+  } else {
+    return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
+  }
+};