diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 41f3ce2ee3..210fbbbb49 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -1,8 +1,9 @@
 # frozen_string_literal: true
 
 class Api::BaseController < ApplicationController
-  DEFAULT_STATUSES_LIMIT = 20
-  DEFAULT_ACCOUNTS_LIMIT = 40
+  DEFAULT_STATUSES_LIMIT       = 20
+  DEFAULT_ACCOUNTS_LIMIT       = 40
+  DEFAULT_EMOJI_REACTION_LIMIT = 10
 
   include RateLimitHeaders
   include AccessTokenTrackingConcern
diff --git a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb
new file mode 100644
index 0000000000..03817c6823
--- /dev/null
+++ b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseController
+  include Authorization
+
+  before_action -> { authorize_if_got_token! :read, :'read:accounts' }
+  before_action :set_status
+  after_action :insert_pagination_headers
+
+  def index
+    @accounts = load_accounts
+    render json: @accounts, each_serializer: REST::EmojiReactionAccountSerializer
+  end
+
+  private
+
+  def load_accounts
+    scope = default_accounts
+    # scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope.merge(paginated_emoji_reactions).to_a
+  end
+
+  def default_accounts
+    EmojiReaction
+      .where(status_id: @status.id)
+      #.where(account: { suspended_at: nil })
+  end
+
+  def paginated_emoji_reactions
+    EmojiReaction.paginate_by_max_id(
+      limit_param(1000), #limit_param(DEFAULT_ACCOUNTS_LIMIT),
+      params[:max_id],
+      params[:since_id]
+    )
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def next_path
+    api_v1_status_emoji_reactioned_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
+  end
+
+  def prev_path
+    api_v1_status_emoji_reactioned_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
+  end
+
+  def pagination_max_id
+    @accounts.last.id
+  end
+
+  def pagination_since_id
+    @accounts.first.id
+  end
+
+  def records_continue?
+    @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
+  end
+
+  def set_status
+    @status = Status.find(params[:status_id])
+    authorize @status, :show?
+  rescue Mastodon::NotPermittedError
+    not_found
+  end
+
+  def pagination_params(core_params)
+    params.slice(:limit).permit(:limit).merge(core_params)
+  end
+end
diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb
index 41523c945a..88a3ac4644 100644
--- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb
+++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb
@@ -40,6 +40,13 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
   private
 
   def create_private(emoji)
+    count = EmojiReaction.where(account: current_account, status: @status).count
+
+    if count >= DEFAULT_EMOJI_REACTION_LIMIT
+      bad_request
+      return
+    end
+
     EmojiReactService.new.call(current_account, @status, emoji)
     render json: @status, serializer: REST::StatusSerializer
   end
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index d9d7e14060..d44c535137 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -33,6 +33,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
 export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
 export const FAVOURITES_FETCH_FAIL    = 'FAVOURITES_FETCH_FAIL';
 
+export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST';
+export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS';
+export const EMOJI_REACTIONS_FETCH_FAIL    = 'EMOJI_REACTIONS_FETCH_FAIL';
+
 export const PIN_REQUEST = 'PIN_REQUEST';
 export const PIN_SUCCESS = 'PIN_SUCCESS';
 export const PIN_FAIL    = 'PIN_FAIL';
@@ -427,6 +431,41 @@ export function fetchFavouritesFail(id, error) {
   };
 }
 
+export function fetchEmojiReactions(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchEmojiReactionsRequest(id));
+
+    api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => {
+      dispatch(importFetchedAccounts(response.data.map((er) => er.account)));
+      dispatch(fetchEmojiReactionsSuccess(id, response.data));
+    }).catch(error => {
+      dispatch(fetchEmojiReactionsFail(id, error));
+    });
+  };
+}
+
+export function fetchEmojiReactionsRequest(id) {
+  return {
+    type: EMOJI_REACTIONS_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchEmojiReactionsSuccess(id, accounts) {
+  return {
+    type: EMOJI_REACTIONS_FETCH_SUCCESS,
+    id,
+    accounts,
+  };
+}
+
+export function fetchEmojiReactionsFail(id, error) {
+  return {
+    type: EMOJI_REACTIONS_FETCH_FAIL,
+    error,
+  };
+}
+
 export function pin(status) {
   return (dispatch, getState) => {
     dispatch(pinRequest(status));
diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx
index 7706c3f88a..6b6fba60cb 100644
--- a/app/javascript/mastodon/components/account.jsx
+++ b/app/javascript/mastodon/components/account.jsx
@@ -39,6 +39,7 @@ class Account extends ImmutablePureComponent {
     actionTitle: PropTypes.string,
     defaultAction: PropTypes.string,
     onActionClick: PropTypes.func,
+    children: PropTypes.object,
   };
 
   static defaultProps = {
@@ -70,7 +71,7 @@ class Account extends ImmutablePureComponent {
   };
 
   render () {
-    const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size } = this.props;
+    const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, children } = this.props;
 
     if (!account) {
       return (
@@ -146,6 +147,10 @@ class Account extends ImmutablePureComponent {
             <DisplayName account={account} />
           </Link>
 
+          <div>
+            {children}
+          </div>
+
           <div className='account__relationship'>
             {buttons}
           </div>
diff --git a/app/javascript/mastodon/components/emoji_view.jsx b/app/javascript/mastodon/components/emoji_view.jsx
new file mode 100644
index 0000000000..ac6e92a2ed
--- /dev/null
+++ b/app/javascript/mastodon/components/emoji_view.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import { injectIntl } from 'react-intl';
+import emojify from '../features/emoji/emoji';
+import classNames from 'classnames';
+
+export default class EmojiView extends React.PureComponent {
+
+  static propTypes = {
+    name: PropTypes.string,
+    url: PropTypes.string,
+    staticUrl: PropTypes.string,
+  };
+
+  render () {
+    const { name, url, staticUrl } = this.props;
+
+    let emojiHtml = null;
+    if (url) {
+      let customEmojis = {};
+      customEmojis[`:${name}:`] = { url, static_url: staticUrl };
+      emojiHtml = emojify(`:${name}:`, customEmojis);
+    } else {
+      emojiHtml = emojify(name);
+    }
+
+    return (
+      <span className='emoji' dangerouslySetInnerHTML={{ __html: emojiHtml }} />
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx
index ce86040549..6e624a6295 100644
--- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx
+++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { injectIntl } from 'react-intl';
 import emojify from '../features/emoji/emoji';
 import classNames from 'classnames';
+import EmojiView from './emoji_view';
 
 class EmojiReactionButton extends React.PureComponent {
 
@@ -32,15 +33,6 @@ class EmojiReactionButton extends React.PureComponent {
   render () {
     const { name, url, staticUrl, count, me } = this.props;
 
-    let emojiHtml = null;
-    if (url) {
-      let customEmojis = {};
-      customEmojis[`:${name}:`] = { url, static_url: staticUrl };
-      emojiHtml = emojify(`:${name}:`, customEmojis);
-    } else {
-      emojiHtml = emojify(name);
-    }
-
     const classList = {
       'emoji-reactions-bar__button': true,
       'toggled': me,
@@ -48,7 +40,9 @@ class EmojiReactionButton extends React.PureComponent {
 
     return (
       <button className={classNames(classList)} type='button' onClick={this.onClick}>
-        <span className='emoji' dangerouslySetInnerHTML={{ __html: emojiHtml }} />
+        <span className='emoji'>
+          <EmojiView name={name} url={url} staticUrl={staticUrl} />
+        </span>
         <span className='count'>{count}</span>
       </button>
     );
diff --git a/app/javascript/mastodon/features/emoji_reactions/index.jsx b/app/javascript/mastodon/features/emoji_reactions/index.jsx
new file mode 100644
index 0000000000..9ea4c11f52
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji_reactions/index.jsx
@@ -0,0 +1,107 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import ColumnHeader from 'mastodon/components/column_header';
+import Icon from 'mastodon/components/icon';
+import { fetchEmojiReactions, fetchFavourites } from 'mastodon/actions/interactions';
+import LoadingIndicator from 'mastodon/components/loading_indicator';
+import ScrollableList from 'mastodon/components/scrollable_list';
+import AccountContainer from 'mastodon/containers/account_container';
+import Column from 'mastodon/features/ui/components/column';
+import { Helmet } from 'react-helmet';
+import EmojiView from '../../components/emoji_view';
+
+const messages = defineMessages({
+  refresh: { id: 'refresh', defaultMessage: 'Refresh' },
+});
+
+const mapStateToProps = (state, props) => { return {
+  accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId]),
+} };
+
+export default @connect(mapStateToProps)
+@injectIntl
+class EmojiReactions extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    multiColumn: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentWillMount () {
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
+    }
+  }
+
+  componentWillReceiveProps (nextProps) {
+    if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
+      this.props.dispatch(fetchEmojiReactions(nextProps.params.statusId));
+    }
+  }
+
+  handleRefresh = () => {
+    this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
+  };
+
+  render () {
+    const { intl, accountIds, multiColumn } = this.props;
+    console.dir(accountIds);
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    let groups = {};
+    for (const emoji_reaction of accountIds) {
+      const key = emoji_reaction.account.id;
+      const value = emoji_reaction;
+      if (!groups[key]) groups[key] = [value];
+      else groups[key].push(value);
+    }
+    console.dir(groups)
+
+    const emptyMessage = <FormattedMessage id='empty_column.emoji_reactions' defaultMessage='No one has reacted with emoji this post yet. When someone does, they will show up here.' />;
+
+    return (
+      <Column bindToDocument={!multiColumn}>
+        <ColumnHeader
+          showBackButton
+          multiColumn={multiColumn}
+          extraButton={(
+            <button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
+          )}
+        />
+
+        <ScrollableList
+          scrollKey='emoji_reactions'
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {Object.keys(groups).map((key, index) =>(
+            <AccountContainer key={index} id={key} withNote={false}>
+              <div style={ { 'max-width': '100px' } }>
+                {groups[key].map((value) => <EmojiView name={value.name} url={value.url} staticUrl={value.static_url} />)}
+              </div>
+            </AccountContainer>
+          ))}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx
index bf77e0da75..942be1b8df 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.jsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx
@@ -49,6 +49,8 @@ class DetailedStatus extends ImmutablePureComponent {
       available: PropTypes.bool,
     }),
     onToggleMediaVisibility: PropTypes.func,
+    onEmojiReact: PropTypes.func,
+    onUnEmojiReact: PropTypes.func,
   };
 
   state = {
@@ -191,7 +193,7 @@ class DetailedStatus extends ImmutablePureComponent {
     let emojiReactionsBar = null;
     if (status.get('emoji_reactions')) {
       const emojiReactions = status.get('emoji_reactions');
-      emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReaction={this.props.onEmojiReaction} OnUnEmojiReaction={this.props.OnUnEmojiReaction} />;
+      emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
     }
 
     if (status.get('application')) {
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 34b88b0ec8..45dd276a6f 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -14,6 +14,7 @@ import {
   pin,
   unpin,
   emojiReact,
+  unEmojiReact,
 } from '../../../actions/interactions';
 import {
   muteStatus,
@@ -98,6 +99,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(emojiReact(status, emoji));
   },
 
+  onUnEmojiReact (status, emoji) {
+    dispatch(unEmojiReact(status, emoji));
+  },
+
   onPin (status) {
     if (status.get('pinned')) {
       dispatch(unpin(status));
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index 2c6728fc05..161d105451 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -24,6 +24,8 @@ import Column from '../ui/components/column';
 import {
   favourite,
   unfavourite,
+  emojiReact,
+  unEmojiReact,
   bookmark,
   unbookmark,
   reblog,
@@ -254,6 +256,16 @@ class Status extends ImmutablePureComponent {
     }
   };
 
+  handleEmojiReact = (status, emoji) => {
+    const { dispatch } = this.props;
+    dispatch(emojiReact(status, emoji));
+  };
+
+  handleUnEmojiReact = (status, emoji) => {
+    const { dispatch } = this.props;
+    dispatch(unEmojiReact(status, emoji));
+  };
+
   handlePin = (status) => {
     if (status.get('pinned')) {
       this.props.dispatch(unpin(status));
@@ -644,6 +656,8 @@ class Status extends ImmutablePureComponent {
                   showMedia={this.state.showMedia}
                   onToggleMediaVisibility={this.handleToggleMediaVisibility}
                   pictureInPicture={pictureInPicture}
+                  onEmojiReact={this.handleEmojiReact}
+                  onUnEmojiReact={this.handleUnEmojiReact}
                 />
 
                 <ActionBar
@@ -651,6 +665,7 @@ class Status extends ImmutablePureComponent {
                   status={status}
                   onReply={this.handleReplyClick}
                   onFavourite={this.handleFavouriteClick}
+                  onEmojiReact={this.handleEmojiReact}
                   onReblog={this.handleReblogClick}
                   onBookmark={this.handleBookmarkClick}
                   onDelete={this.handleDeleteClick}
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 4f0ea04504..dc8ed444a5 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -36,6 +36,7 @@ import {
   Following,
   Reblogs,
   Favourites,
+  EmojiReactions,
   DirectTimeline,
   HashtagTimeline,
   Notifications,
@@ -206,6 +207,7 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
           <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
           <WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
+          <WrappedRoute path='/@:acct/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
 
           {/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
           <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
@@ -213,6 +215,7 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
           <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
           <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
+          <WrappedRoute path='/statuses/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
 
           <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
           <WrappedRoute path='/blocks' component={Blocks} content={children} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 1cf07f6453..5e175c9413 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -78,6 +78,10 @@ export function Favourites () {
   return import(/* webpackChunkName: "features/favourites" */'../../favourites');
 }
 
+export function EmojiReactions () {
+  return import(/* webpackChunkName: "features/favourites" */'../../emoji_reactions');
+}
+
 export function FollowRequests () {
   return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
 }
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index 2a80cf6398..b57cbe3823 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -26,6 +26,7 @@ import {
 import {
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
+  EMOJI_REACTIONS_FETCH_SUCCESS,
 } from '../actions/interactions';
 import {
   BLOCKS_FETCH_REQUEST,
@@ -69,6 +70,7 @@ const initialState = ImmutableMap({
   following: initialListState,
   reblogged_by: initialListState,
   favourited_by: initialListState,
+  emoji_reactioned_by: initialListState,
   follow_requests: initialListState,
   blocks: initialListState,
   mutes: initialListState,
@@ -133,6 +135,10 @@ export default function userLists(state = initialState, action) {
     return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FAVOURITES_FETCH_SUCCESS:
     return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+  case EMOJI_REACTIONS_FETCH_SUCCESS:
+    console.log('===================')
+    console.dir(state);
+    return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts));
   case NOTIFICATIONS_UPDATE:
     return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
   case FOLLOW_REQUESTS_FETCH_SUCCESS:
diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb
index db3cb91b3f..df8cea913d 100644
--- a/app/lib/activitypub/activity/like.rb
+++ b/app/lib/activitypub/activity/like.rb
@@ -42,8 +42,11 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
 
     return if @account.reacted?(@original_status, shortcode, emoji)
 
+    return if EmojiReaction.where(account: @account, status: @original_status).count >= BaseController::DEFAULT_EMOJI_REACTION_LIMIT
+
     EmojiReaction.find_by(account: @account, status: @original_status)&.destroy
     reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id'])
+    write_stream(reaction)
 
     if @original_status.account.local?
       NotifyService.new.call(@original_status.account, :emoji_reaction, reaction)
@@ -91,4 +94,15 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
 
     @emoji_tag = @json['tag'].is_a?(Array) ? @json['tag']&.first : @json['tag']
   end
+
+  def write_stream(emoji_reaction)
+    emoji_group = @original_status.emoji_reactions_grouped_by_name
+                                  .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
+    emoji_group['status_id'] = @original_status.id.to_s
+    FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id)
+  end
+
+  def render_emoji_reaction(emoji_group)
+    @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json)
+  end
 end
diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb
index 09a7ac7a30..a8558f457d 100644
--- a/app/lib/activitypub/activity/undo.rb
+++ b/app/lib/activitypub/activity/undo.rb
@@ -140,6 +140,23 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
     end
   end
 
+  def write_stream(emoji_reaction)
+    emoji_group = @original_status.emoji_reactions_grouped_by_name
+                                  .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
+    if emoji_group
+      emoji_group['status_id'] = @original_status.id.to_s
+    else
+      # name: emoji_reaction.name, count: 0, domain: emoji_reaction.domain
+      emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @status.id.to_s }
+      emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
+    end
+    FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id)
+  end
+
+  def render_emoji_reaction(emoji_group)
+    @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json)
+  end
+
   def forward_for_undo_emoji_reaction
     return unless @json['signature'].present?
 
diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb
index 4dbeab1946..b66751ffed 100644
--- a/app/models/emoji_reaction.rb
+++ b/app/models/emoji_reaction.rb
@@ -40,4 +40,11 @@ class EmojiReaction < ApplicationRecord
 
     account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav)
   end
+
+  def paginate_by_max_id(limit, max_id = nil, since_id = nil)
+    query = order(arel_table[:id].desc).limit(limit)
+    query = query.where(arel_table[:id].lt(max_id)) if max_id.present?
+    query = query.where(arel_table[:id].gt(since_id)) if since_id.present?
+    query
+  end
 end
diff --git a/app/serializers/rest/emoji_reaction_account_serializer.rb b/app/serializers/rest/emoji_reaction_account_serializer.rb
new file mode 100644
index 0000000000..1925754077
--- /dev/null
+++ b/app/serializers/rest/emoji_reaction_account_serializer.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class REST::EmojiReactionAccountSerializer < ActiveModel::Serializer
+  include RoutingHelper
+  include FormattingHelper
+
+  attributes :id, :name
+
+  attribute :url, if: :custom_emoji?
+  attribute :static_url, if: :custom_emoji?
+  attribute :domain, if: :custom_emoji?
+
+  belongs_to :account, serializer: REST::AccountSerializer
+
+  def id
+    object.id.to_s
+  end
+
+  def url
+    full_asset_url(object.custom_emoji.image.url)
+  end
+
+  def static_url
+    full_asset_url(object.custom_emoji.image.url(:static))
+  end
+
+  def domain
+    object.custom_emoji.domain
+  end
+
+  def custom_emoji?
+    object.custom_emoji.present?
+  end
+end
diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb
index a2d535d262..b1e0833a56 100644
--- a/app/services/delete_account_service.rb
+++ b/app/services/delete_account_service.rb
@@ -147,6 +147,7 @@ class DeleteAccountService < BaseService
     purge_polls!
     purge_generated_notifications!
     purge_favourites!
+    purge_emoji_reactions!
     purge_bookmarks!
     purge_feeds!
     purge_other_associations!
@@ -193,6 +194,16 @@ class DeleteAccountService < BaseService
     end
   end
 
+  def purge_emoji_reactions!
+    @account.emoji_reactions.in_batches do |reactions|
+      reactions.each do |reaction|
+        reaction.status.refresh_emoji_reactions_grouped_by_name
+      end
+      Chewy.strategy.current.update(StatusesIndex, reactions.pluck(:status_id)) if Chewy.enabled?
+      reactions.delete_all
+    end
+  end
+
   def purge_bookmarks!
     @account.bookmarks.in_batches do |bookmarks|
       Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled?
diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb
index cf746bb0ef..d8f51548a0 100644
--- a/app/services/emoji_react_service.rb
+++ b/app/services/emoji_react_service.rb
@@ -55,7 +55,7 @@ class EmojiReactService < BaseService
 
   def write_stream(emoji_reaction)
     emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name
-                                .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) }
+                                .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
     emoji_group['status_id'] = emoji_reaction.status_id.to_s
     FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id)
   end
diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb
index ca58c3c1d0..5a2e9eaf1c 100644
--- a/app/services/un_emoji_react_service.rb
+++ b/app/services/un_emoji_react_service.rb
@@ -40,7 +40,7 @@ class UnEmojiReactService < BaseService
 
   def write_stream(emoji_reaction)
     emoji_group = @status.emoji_reactions_grouped_by_name
-                                .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) }
+                                .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
     if emoji_group
       emoji_group['status_id'] = @status.id.to_s
     else
diff --git a/config/routes.rb b/config/routes.rb
index 9b79366b74..55654695d0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,7 @@ Rails.application.routes.draw do
     /lists/(*any)
     /notifications
     /favourites
+    /emoji_reactions
     /bookmarks
     /pinned
     /start
@@ -439,6 +440,7 @@ Rails.application.routes.draw do
         scope module: :statuses do
           resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
           resources :favourited_by, controller: :favourited_by_accounts, only: :index
+          resources :emoji_reactioned_by, controller: :emoji_reactioned_by_accounts, only: :index
           resource :reblog, only: :create
           post :unreblog, to: 'reblogs#destroy'