diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb
new file mode 100644
index 0000000000..efeb8e9929
--- /dev/null
+++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
+  include Authorization
+
+  before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' }
+  before_action :require_user!
+  before_action :set_status, only: [:update]
+
+  # For compatible with Fedibird API
+  def update
+    create_private
+  end
+
+  # TODO: destroy emoji reaction api
+  def destroy
+    # fav = current_account.favourites.find_by(status_id: params[:status_id])
+
+    # if fav
+    #  @status = fav.status
+    #  UnfavouriteWorker.perform_async(current_account.id, @status.id)
+    # else
+    #  @status = Status.find(params[:status_id])
+    #  authorize @status, :show?
+    # end
+
+    # render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false })
+    # rescue Mastodon::NotPermittedError
+    not_found
+  end
+
+  private
+
+  def create_private
+    EmojiReactService.new.call(current_account, @status, params[:id])
+    render json: @status, serializer: REST::StatusSerializer
+  end
+
+  def set_status
+    @status = Status.find(params[:status_id])
+    authorize @status, :show?
+  rescue Mastodon::NotPermittedError
+    not_found
+  end
+end
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index bc35736ffa..15b466141c 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -9,6 +9,10 @@ export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
 export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
 export const FAVOURITE_FAIL    = 'FAVOURITE_FAIL';
 
+export const EMOJIREACT_REQUEST = 'EMOJIREACT_REQUEST';
+export const EMOJIREACT_SUCCESS = 'EMOJIREACT_SUCCESS';
+export const EMOJIREACT_FAIL    = 'EMOJIREACT_FAIL';
+
 export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
 export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
 export const UNREBLOG_FAIL    = 'UNREBLOG_FAIL';
@@ -17,6 +21,10 @@ export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
 export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
 export const UNFAVOURITE_FAIL    = 'UNFAVOURITE_FAIL';
 
+export const UNEMOJIREACT_REQUEST = 'UNEMOJIREACT_REQUEST';
+export const UNEMOJIREACT_SUCCESS = 'UNEMOJIREACT_SUCCESS';
+export const UNEMOJIREACT_FAIL    = 'UNEMOJIREACT_FAIL';
+
 export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
 export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
 export const REBLOGS_FETCH_FAIL    = 'REBLOGS_FETCH_FAIL';
@@ -195,6 +203,89 @@ export function unfavouriteFail(status, error) {
   };
 }
 
+export function emojiReact(status, emoji) {
+  return function (dispatch, getState) {
+    dispatch(emojiReactRequest(status, emoji));
+    console.dir(emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native);
+
+    api(getState).put(`/api/v1/statuses/${status.get('id')}/emoji_reactions/${emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native}`).then(function (response) {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(emojiReactSuccess(status, emoji));
+    }).catch(function (error) {
+      dispatch(emojiReactFail(status, emoji, error));
+    });
+  };
+}
+
+export function unEmojiReact(status, emoji) {
+  return (dispatch, getState) => {
+    dispatch(unEmojiReactRequest(status, emoji));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreactions/${emoji.native}`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unEmojiReactSuccess(status, emoji));
+    }).catch(error => {
+      dispatch(unEmojiReactFail(status, emoji, error));
+    });
+  };
+}
+
+export function emojiReactRequest(status, emoji) {
+  return {
+    type: EMOJIREACT_REQUEST,
+    status: status,
+    emoji: emoji,
+    skipLoading: true,
+  };
+}
+
+export function emojiReactSuccess(status, emoji) {
+  return {
+    type: EMOJIREACT_SUCCESS,
+    status: status,
+    emoji: emoji,
+    skipLoading: true,
+  };
+}
+
+export function emojiReactFail(status, emoji, error) {
+  return {
+    type: EMOJIREACT_FAIL,
+    status: status,
+    emoji: emoji,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function unEmojiReactRequest(status, emoji) {
+  return {
+    type: UNEMOJIREACT_REQUEST,
+    status: status,
+    emoji: emoji,
+    skipLoading: true,
+  };
+}
+
+export function unEmojiReactSuccess(status, emoji) {
+  return {
+    type: UNEMOJIREACT_SUCCESS,
+    status: status,
+    emoji: emoji,
+    skipLoading: true,
+  };
+}
+
+export function unEmojiReactFail(status, emoji, error) {
+  return {
+    type: UNEMOJIREACT_FAIL,
+    status: status,
+    emoji: emoji,
+    error: error,
+    skipLoading: true,
+  };
+}
+
 export function bookmark(status) {
   return function (dispatch, getState) {
     dispatch(bookmarkRequest(status));
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 28b1cf321d..ceda971feb 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -73,6 +73,8 @@ class Status extends ImmutablePureComponent {
     onClick: PropTypes.func,
     onReply: PropTypes.func,
     onFavourite: PropTypes.func,
+    onEmojiReact: PropTypes.func,
+    onUnEmojiReact: PropTypes.func,
     onReblog: PropTypes.func,
     onDelete: PropTypes.func,
     onDirect: PropTypes.func,
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index f6967219dc..282892809c 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -68,6 +68,7 @@ class StatusActionBar extends ImmutablePureComponent {
     relationship: ImmutablePropTypes.map,
     onReply: PropTypes.func,
     onFavourite: PropTypes.func,
+    onEmojiReact: PropTypes.func,
     onReblog: PropTypes.func,
     onDelete: PropTypes.func,
     onDirect: PropTypes.func,
@@ -129,6 +130,16 @@ class StatusActionBar extends ImmutablePureComponent {
     }
   };
 
+  handleEmojiPick = (data) => {
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      this.props.onEmojiReact(this.props.status, data);
+    } else {
+      this.props.onInteractionModal('favourite', this.props.status);
+    }
+  };
+
   handleReblogClick = e => {
     const { signedIn } = this.context.identity;
 
@@ -232,16 +243,6 @@ class StatusActionBar extends ImmutablePureComponent {
     this.props.onFilter();
   };
 
-  handleEmojiPick = (data) => {
-    /*
-    const { text }     = this.props;
-    const position     = this.autosuggestTextarea.textarea.selectionStart;
-    const needsSpace   = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
-
-    this.props.onPickEmoji(position, data, needsSpace);
-    */
-  };
-
   render () {
     const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
     const { signedIn, permissions } = this.context.identity;
diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx
index 9761fcca39..d91525430d 100644
--- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx
+++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx
@@ -8,7 +8,7 @@ import classNames from 'classnames';
 class EmojiReactionButton extends React.PureComponent {
 
   static propTypes = {
-    name: ImmutablePropTypes.map,
+    name: PropTypes.string,
     url: PropTypes.string,
     staticUrl: PropTypes.string,
     count: PropTypes.number.isRequired,
@@ -22,7 +22,7 @@ class EmojiReactionButton extends React.PureComponent {
     let emojiHtml = null;
     if (url) {
       let customEmojis = {};
-      customEmojis[name] = { url, static_url: staticUrl };
+      customEmojis[`:${name}:`] = { url, static_url: staticUrl };
       emojiHtml = emojify(`:${name}:`, customEmojis);
     } else {
       emojiHtml = emojify(name);
@@ -47,16 +47,16 @@ export default @injectIntl
 class StatusEmojiReactionsBar extends React.PureComponent {
 
   static propTypes = {
-    emojiReactions: ImmutablePropTypes.map.isRequired,
+    emojiReactions: ImmutablePropTypes.list.isRequired,
     statusId: PropTypes.string,
   };
 
   render () {
     const { emojiReactions, statusId } = this.props;
 
-    const emojiButtons = React.Children.map(emojiReactions, (emoji) => (
+    const emojiButtons = Array.from(emojiReactions).map((emoji, index) => (
       <EmojiReactionButton
-        key={emoji.get('id')}
+        key={index}
         name={emoji.get('name')}
         count={emoji.get('count')}
         me={emoji.get('me')}
diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx
index 294105f259..f3a694a8f2 100644
--- a/app/javascript/mastodon/containers/status_container.jsx
+++ b/app/javascript/mastodon/containers/status_container.jsx
@@ -10,9 +10,11 @@ import {
 import {
   reblog,
   favourite,
+  emojiReact,
   bookmark,
   unreblog,
   unfavourite,
+  unEmojiReact,
   unbookmark,
   pin,
   unpin,
@@ -113,6 +115,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     }
   },
 
+  onEmojiReact (status, emoji) {
+    dispatch(emojiReact(status, emoji));
+  },
+
+  onUnEmojiReact (status, emoji) {
+    dispatch(unEmojiReact(status, emoji));
+  },
+
   onBookmark (status) {
     if (status.get('bookmarked')) {
       dispatch(unbookmark(status));
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js
index 8c5688acba..9220d0ad95 100644
--- a/app/javascript/mastodon/features/notifications/containers/notification_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js
@@ -61,6 +61,10 @@ const mapDispatchToProps = dispatch => ({
     }
   },
 
+  onEmojiReact (status, emoji) {
+    
+  },
+
   onToggleHidden (status) {
     if (status.get('hidden')) {
       dispatch(revealStatus(status.get('id')));
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 bfed166200..546f60db80 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -93,6 +93,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onEmojiReact (status, emoji) {
+    
+  },
+
   onPin (status) {
     if (status.get('pinned')) {
       dispatch(unpin(status));
diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb
index f5bc203465..4fbdec5bbf 100644
--- a/app/lib/potential_friendship_tracker.rb
+++ b/app/lib/potential_friendship_tracker.rb
@@ -6,6 +6,7 @@ class PotentialFriendshipTracker
 
   WEIGHTS = {
     reply: 1,
+    emoji_reaction: 5,
     favourite: 10,
     reblog: 20,
   }.freeze
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index bbe269e8f0..3435f7a9e5 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -13,6 +13,7 @@ module AccountAssociations
     # Timelines
     has_many :statuses, inverse_of: :account, dependent: :destroy
     has_many :favourites, inverse_of: :account, dependent: :destroy
+    has_many :emoji_reactions, inverse_of: :account, dependent: :destroy
     has_many :bookmarks, inverse_of: :account, dependent: :destroy
     has_many :mentions, inverse_of: :account, dependent: :destroy
     has_many :notifications, inverse_of: :account, dependent: :destroy
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 3d7900226d..3bd8c0b895 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -37,6 +37,7 @@ class CustomEmoji < ApplicationRecord
 
   belongs_to :category, class_name: 'CustomEmojiCategory', optional: true
   has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode
+  has_many :emoji_reactions, inverse_of: :custom_emoji, dependent: :destroy
 
   has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set modify-date +set create-date' } }, validate_media_type: false
 
diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb
index 30b83e61ec..4dbeab1946 100644
--- a/app/models/emoji_reaction.rb
+++ b/app/models/emoji_reaction.rb
@@ -21,7 +21,7 @@ class EmojiReaction < ApplicationRecord
 
   belongs_to :account,       inverse_of: :emoji_reactions
   belongs_to :status,        inverse_of: :emoji_reactions
-  belongs_to :custom_emojis, optional: true
+  belongs_to :custom_emoji,  optional: true
 
   has_one :notification, as: :activity, dependent: :destroy
 
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 3eaf557b08..88c5f44b6c 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -60,6 +60,7 @@ class Notification < ApplicationRecord
   belongs_to :follow,         foreign_key: 'activity_id', optional: true
   belongs_to :follow_request, foreign_key: 'activity_id', optional: true
   belongs_to :favourite,      foreign_key: 'activity_id', optional: true
+  belongs_to :emoji_reaction, foreign_key: 'activity_id', optional: true
   belongs_to :poll,           foreign_key: 'activity_id', optional: true
   belongs_to :report,         foreign_key: 'activity_id', optional: true
 
@@ -79,6 +80,8 @@ class Notification < ApplicationRecord
       status&.reblog
     when :favourite
       favourite&.status
+    when :emoji_reaction
+      emoji_reaction&.status
     when :mention
       mention&.status
     when :poll
@@ -128,6 +131,8 @@ class Notification < ApplicationRecord
           notification.status.reblog = cached_status
         when :favourite
           notification.favourite.status = cached_status
+        when :emoji_reaction
+          notification.emoji_reaction.status = cached_status
         when :mention
           notification.mention.status = cached_status
         when :poll
@@ -148,7 +153,7 @@ class Notification < ApplicationRecord
     return unless new_record?
 
     case activity_type
-    when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
+    when 'Status', 'Follow', 'Favourite', 'EmojiReaction', 'FollowRequest', 'Poll', 'Report'
       self.from_account_id = activity&.account_id
     when 'Mention'
       self.from_account_id = activity&.status&.account_id
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index f3d0ffdbae..694997788d 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -27,6 +27,10 @@ class StatusPolicy < ApplicationPolicy
     show? && !blocking_author?
   end
 
+  def emoji_reaction?
+    show? && !blocking_author?
+  end
+
   def destroy?
     owned?
   end
diff --git a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb
index c6d21cdaf6..d4230de61a 100644
--- a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb
+++ b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
   attributes :name, :count
 
   attribute :me, if: :current_user?
@@ -14,10 +16,22 @@ class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer
   end
 
   def custom_emoji?
-    object.respond_to?(:custom_emoji)
+    object.custom_emoji.present?
   end
 
   def account_ids?
     object.respond_to?(:account_ids)
   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
 end
diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb
new file mode 100644
index 0000000000..617edd44cc
--- /dev/null
+++ b/app/services/emoji_react_service.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+class EmojiReactService < BaseService
+  include Authorization
+  include Payloadable
+
+  # React a status with emoji and notify remote user
+  # @param [Account] account
+  # @param [Status] status
+  # @param [string] name
+  # @return [Favourite]
+  def call(account, status, name)
+    authorize_with account, status, :emoji_reaction?
+
+    emoji_reaction = EmojiReaction.find_by(account: account, status: status, name: name)
+
+    return emoji_reaction unless emoji_reaction.nil?
+
+    shortcode, domain = name.split('@')
+
+    custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
+
+    emoji_reaction = EmojiReaction.create!(account: account, status: status, name: shortcode, custom_emoji: custom_emoji)
+
+    Trends.statuses.register(status)
+
+    create_notification(emoji_reaction)
+    notify_to_followers(emoji_reaction)
+    bump_potential_friendship(account, status)
+
+    emoji_reaction
+  end
+
+  private
+
+  def create_notification(emoji_reaction)
+    status = emoji_reaction.status
+
+    if status.account.local?
+      # TODO: Change favourite event to notify
+      LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'Favourite', 'favourite')
+    elsif status.account.activitypub?
+      ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url)
+    end
+  end
+
+  def notify_to_followers(emoji_reaction)
+    status = emoji_reaction.status
+
+    return unless status.account.local?
+
+    ActivityPub::RawDistributionWorker.perform_async(emoji_reaction, status.account_id)
+  end
+
+  def broadcast_updates!(emoji_reaction)
+    status = emoji_reaction.status
+
+    DistributionWorker.perform_async(status.id, { 'update' => true })
+  end
+
+  def bump_potential_friendship(account, status)
+    ActivityTracker.increment('activity:interactions')
+    return if account.following?(status.account_id)
+
+    PotentialFriendshipTracker.record(account.id, status.account_id, :emoji_reaction)
+  end
+
+  def build_json(emoji_reaction)
+    # TODO: change to original serializer for other servers
+    Oj.dump(serialize_payload(emoji_reaction, ActivityPub::LikeSerializer))
+  end
+end
diff --git a/config/routes.rb b/config/routes.rb
index e7c4c000ee..a9c6e96ed7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -458,6 +458,9 @@ Rails.application.routes.draw do
           resource :source, only: :show
 
           post :translate, to: 'translations#create'
+
+          resources :emoji_reactions, only: :update, constraints: { id: /[^\/]+/ }
+          post :emoji_unreaction, to: 'emoji_reactions#destroy'
         end
 
         member do