diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 5bc7f8d9cb95e173f533e275734c9bb2bbe8297c..da99394ff908f90d753a23ff04fa5fc2aadbd7c7 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -2,11 +2,8 @@ import Cookies from 'js-cookie';
 import bp from './breakpoints';
 import UsersSelect from './users_select';
 
-const PARTICIPANTS_ROW_COUNT = 7;
-
 export default class IssuableContext {
   constructor(currentUser) {
-    this.initParticipants();
     this.userSelect = new UsersSelect(currentUser);
 
     $('select.select2').select2({
@@ -51,29 +48,4 @@ export default class IssuableContext {
       }
     });
   }
-
-  initParticipants() {
-    $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
-    return $('.js-participants-author').each(function forEachAuthor(i) {
-      if (i >= PARTICIPANTS_ROW_COUNT) {
-        $(this).addClass('js-participants-hidden').hide();
-      }
-    });
-  }
-
-  toggleHiddenParticipants() {
-    const currentText = $(this).text().trim();
-    const lessText = $(this).data('less-text');
-    const originalText = $(this).data('original-text');
-
-    if (currentText === originalText) {
-      $(this).text(lessText);
-
-      if (gl.lazyLoader) gl.lazyLoader.loadCheck();
-    } else {
-      $(this).text(originalText);
-    }
-
-    $('.js-participants-hidden').toggle();
-  }
 }
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b8510a6ce3a9d5349ac871b9ce6bacf104c9ab26
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -0,0 +1,125 @@
+<script>
+import { __, n__, sprintf } from '../../../locale';
+import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+
+export default {
+  props: {
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    participants: {
+      type: Array,
+      required: false,
+      default: () => [],
+    },
+    numberOfLessParticipants: {
+      type: Number,
+      required: false,
+      default: 7,
+    },
+  },
+  data() {
+    return {
+      isShowingMoreParticipants: false,
+    };
+  },
+  components: {
+    loadingIcon,
+    userAvatarImage,
+  },
+  computed: {
+    lessParticipants() {
+      return this.participants.slice(0, this.numberOfLessParticipants);
+    },
+    visibleParticipants() {
+      return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
+    },
+    hasMoreParticipants() {
+      return this.participants.length > this.numberOfLessParticipants;
+    },
+    toggleLabel() {
+      let label = '';
+      if (this.isShowingMoreParticipants) {
+        label = __('- show less');
+      } else {
+        label = sprintf(__('+ %{moreCount} more'), {
+          moreCount: this.participants.length - this.numberOfLessParticipants,
+        });
+      }
+
+      return label;
+    },
+    participantLabel() {
+      return sprintf(
+        n__('%{count} participant', '%{count} participants', this.participants.length),
+        { count: this.loading ? '' : this.participantCount },
+      );
+    },
+    participantCount() {
+      return this.participants.length;
+    },
+  },
+  methods: {
+    toggleMoreParticipants() {
+      this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <div class="sidebar-collapsed-icon">
+      <i
+        class="fa fa-users"
+        aria-hidden="true">
+      </i>
+      <loading-icon
+        v-if="loading"
+        class="js-participants-collapsed-loading-icon" />
+      <span
+        v-else
+        class="js-participants-collapsed-count">
+        {{ participantCount }}
+      </span>
+    </div>
+    <div class="title hide-collapsed">
+      <loading-icon
+        v-if="loading"
+        :inline="true"
+        class="js-participants-expanded-loading-icon" />
+      {{ participantLabel }}
+    </div>
+    <div class="participants-list hide-collapsed">
+      <div
+        v-for="participant in visibleParticipants"
+        :key="participant.id"
+        class="participants-author js-participants-author">
+        <a
+          class="author_link"
+          :href="participant.web_url">
+          <user-avatar-image
+            :lazy="true"
+            :img-src="participant.avatar_url"
+            css-classes="avatar-inline"
+            :size="24"
+            :tooltip-text="participant.name"
+            tooltip-placement="bottom" />
+        </a>
+      </div>
+    </div>
+    <div
+      v-if="hasMoreParticipants"
+      class="participants-more hide-collapsed">
+      <button
+        type="button"
+        class="btn-transparent btn-blank js-toggle-participants-button"
+        @click="toggleMoreParticipants">
+        {{ toggleLabel }}
+      </button>
+    </div>
+  </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c1296b28db73f246d63f2d978df92ce937cb03cc
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -0,0 +1,26 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import participants from './participants.vue';
+
+export default {
+  data() {
+    return {
+      mediator: new Mediator(),
+      store: new Store(),
+    };
+  },
+  components: {
+    participants,
+  },
+};
+</script>
+
+<template>
+  <div class="block participants">
+    <participants
+      :loading="store.isFetching.participants"
+      :participants="store.participants"
+      :number-of-less-participants="7" />
+  </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4ad3d469f25e884db472c95e554d6aa599310b31
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -0,0 +1,45 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import eventHub from '../../event_hub';
+import Flash from '../../../flash';
+import subscriptions from './subscriptions.vue';
+
+export default {
+  data() {
+    return {
+      mediator: new Mediator(),
+      store: new Store(),
+    };
+  },
+
+  components: {
+    subscriptions,
+  },
+
+  methods: {
+    onToggleSubscription() {
+      this.mediator.toggleSubscription()
+        .catch(() => {
+          Flash('Error occurred when toggling the notification subscription');
+        });
+    },
+  },
+
+  created() {
+    eventHub.$on('toggleSubscription', this.onToggleSubscription);
+  },
+
+  beforeDestroy() {
+    eventHub.$off('toggleSubscription', this.onToggleSubscription);
+  },
+};
+</script>
+
+<template>
+  <div class="block subscriptions">
+    <subscriptions
+      :loading="store.isFetching.subscriptions"
+      :subscribed="store.subscribed" />
+  </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a3a8213d63a3eb8b8f20b6f28f553c68bb64eb3a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -0,0 +1,60 @@
+<script>
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
+import loadingButton from '../../../vue_shared/components/loading_button.vue';
+
+export default {
+  props: {
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    subscribed: {
+      type: Boolean,
+      required: false,
+    },
+  },
+  components: {
+    loadingButton,
+  },
+  computed: {
+    buttonLabel() {
+      let label;
+      if (this.subscribed === false) {
+        label = __('Subscribe');
+      } else if (this.subscribed === true) {
+        label = __('Unsubscribe');
+      }
+
+      return label;
+    },
+  },
+  methods: {
+    toggleSubscription() {
+      eventHub.$emit('toggleSubscription');
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <div class="sidebar-collapsed-icon">
+      <i
+        class="fa fa-rss"
+        aria-hidden="true">
+      </i>
+    </div>
+    <span class="issuable-header-text hide-collapsed pull-left">
+      {{ __('Notifications') }}
+    </span>
+    <loading-button
+      ref="loadingButton"
+      class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
+      :loading="loading"
+      :label="buttonLabel"
+      @click="toggleSubscription"
+    />
+  </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 604648407a4bcaefad3a1503f9af7bb2bfd34650..37c97225bfde7bd90e4fe429bd6f3f43e8e8843a 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -7,6 +7,7 @@ export default class SidebarService {
   constructor(endpointMap) {
     if (!SidebarService.singleton) {
       this.endpoint = endpointMap.endpoint;
+      this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
       this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
       this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
 
@@ -36,6 +37,10 @@ export default class SidebarService {
     });
   }
 
+  toggleSubscription() {
+    return Vue.http.post(this.toggleSubscriptionEndpoint);
+  }
+
   moveIssue(moveToProjectId) {
     return Vue.http.post(this.moveIssueEndpoint, {
       move_to_project_id: moveToProjectId,
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 09b9d75c02d093d04ad12754638affd5b91695fe..2650bb725d404bea28fc551ef3742d6b21b6b8c7 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
 import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
 import SidebarMoveIssue from './lib/sidebar_move_issue';
 import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import sidebarParticipants from './components/participants/sidebar_participants.vue';
+import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
 import Translate from '../vue_shared/translate';
 
 import Mediator from './sidebar_mediator';
@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
   }).$mount(el);
 }
 
+function mountParticipantsComponent() {
+  const el = document.querySelector('.js-sidebar-participants-entry-point');
+
+  if (!el) return;
+
+  // eslint-disable-next-line no-new
+  new Vue({
+    el,
+    components: {
+      sidebarParticipants,
+    },
+    render: createElement => createElement('sidebar-participants', {}),
+  });
+}
+
+function mountSubscriptionsComponent() {
+  const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+
+  if (!el) return;
+
+  // eslint-disable-next-line no-new
+  new Vue({
+    el,
+    components: {
+      sidebarSubscriptions,
+    },
+    render: createElement => createElement('sidebar-subscriptions', {}),
+  });
+}
+
 function domContentLoaded() {
   const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
   const mediator = new Mediator(sidebarOptions);
@@ -63,6 +95,8 @@ function domContentLoaded() {
 
   mountConfidentialComponent(mediator);
   mountLockComponent(mediator);
+  mountParticipantsComponent();
+  mountSubscriptionsComponent();
 
   new SidebarMoveIssue(
     mediator,
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index ede3a0de144f646268a7465c002a8dbe54bd5391..2bda5a47791d8f8954b3d7b942bdb28bf5cf59b2 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -8,6 +8,7 @@ export default class SidebarMediator {
       this.store = new Store(options);
       this.service = new Service({
         endpoint: options.endpoint,
+        toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
         moveIssueEndpoint: options.moveIssueEndpoint,
         projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
       });
@@ -39,10 +40,25 @@ export default class SidebarMediator {
       .then((data) => {
         this.store.setAssigneeData(data);
         this.store.setTimeTrackingData(data);
+        this.store.setParticipantsData(data);
+        this.store.setSubscriptionsData(data);
       })
       .catch(() => new Flash('Error occurred when fetching sidebar data'));
   }
 
+  toggleSubscription() {
+    this.store.setFetchingState('subscriptions', true);
+    return this.service.toggleSubscription()
+      .then(() => {
+        this.store.setSubscribedState(!this.store.subscribed);
+        this.store.setFetchingState('subscriptions', false);
+      })
+      .catch((err) => {
+        this.store.setFetchingState('subscriptions', false);
+        throw err;
+      });
+  }
+
   fetchAutocompleteProjects(searchTerm) {
     return this.service.getProjectsAutocomplete(searchTerm)
       .then(response => response.json())
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index d5d04103f3f03f435a5e227e75ba3507d3029c1b..3150221b6855ace53e87a2931be95a9ad86969e7 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -12,10 +12,14 @@ export default class SidebarStore {
       this.assignees = [];
       this.isFetching = {
         assignees: true,
+        participants: true,
+        subscriptions: true,
       };
       this.autocompleteProjects = [];
       this.moveToProjectId = 0;
       this.isLockDialogOpen = false;
+      this.participants = [];
+      this.subscribed = null;
 
       SidebarStore.singleton = this;
     }
@@ -37,6 +41,20 @@ export default class SidebarStore {
     this.humanTotalTimeSpent = data.human_total_time_spent;
   }
 
+  setParticipantsData(data) {
+    this.isFetching.participants = false;
+    this.participants = data.participants || [];
+  }
+
+  setSubscriptionsData(data) {
+    this.isFetching.subscriptions = false;
+    this.subscribed = data.subscribed || false;
+  }
+
+  setFetchingState(key, value) {
+    this.isFetching[key] = value;
+  }
+
   addAssignee(assignee) {
     if (!this.findAssignee(assignee)) {
       this.assignees.push(assignee);
@@ -61,6 +79,10 @@ export default class SidebarStore {
     this.autocompleteProjects = projects;
   }
 
+  setSubscribedState(subscribed) {
+    this.subscribed = subscribed;
+  }
+
   setMoveToProjectId(moveToProjectId) {
     this.moveToProjectId = moveToProjectId;
   }
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 79c3d335679a5d8ac00799cfa417e84e3282d1a1..99f5c305df53b0b7d184528c2f93a1999b11a46f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -11,7 +11,7 @@ export default class MRWidgetService {
     this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
     this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
     this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
-    this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
+    this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
     this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
   }
 
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 485325032639391c2bfdc9a1b067facc288bcc51..88600a0e6d346fcf70e0dfe7a8f140ec81424d32 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -542,7 +542,9 @@
 }
 
 .participants-list {
-  margin: -5px;
+  display: flex;
+  flex-wrap: wrap;
+  margin: -7px;
 }
 
 
@@ -553,7 +555,7 @@
 
 .participants-author {
   display: inline-block;
-  padding: 5px;
+  padding: 7px;
 
   &:nth-of-type(7n) {
     padding-right: 0;
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index fe1334c0cfe2d79d33d256dbcb00fcf23e80151f..6a5e4538717badeee1c38f996eaeb53d796ee2cf 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
     respond_to do |format|
       format.html
       format.json do
-        render json: serializer.represent(@issue)
+        render json: serializer.represent(@issue, serializer: params[:serializer])
       end
     end
   end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c5204080333b6ac6d369ab0af3defb921f3c7725..2b0294c8387bf293ead2fc4b14f1e40c7bbc99a4 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
       format.json do
         Gitlab::PollingInterval.set_header(response, interval: 10_000)
 
-        render json: serializer.represent(@merge_request, basic: params[:basic])
+        render json: serializer.represent(@merge_request, serializer: params[:serializer])
       end
 
       format.patch  do
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index baa2d6e375eb88f39ae326db1d8366faf394c3a1..d0069cd48cfd5397355187ea4afabe9ab364a881 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -33,15 +33,17 @@ module IssuablesHelper
   end
 
   def serialize_issuable(issuable)
-    case issuable
-    when Issue
-      IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json
-    when MergeRequest
-      MergeRequestSerializer
-        .new(current_user: current_user, project: issuable.project)
-        .represent(issuable)
-        .to_json
-    end
+    serializer_klass = case issuable
+                       when Issue
+                         IssueSerializer
+                       when MergeRequest
+                         MergeRequestSerializer
+                       end
+
+    serializer_klass
+      .new(current_user: current_user, project: issuable.project)
+      .represent(issuable)
+      .to_json
   end
 
   def template_dropdown_tag(issuable, &block)
@@ -357,7 +359,8 @@ module IssuablesHelper
 
   def issuable_sidebar_options(issuable, can_edit_issuable)
     {
-      endpoint: "#{issuable_json_path(issuable)}?basic=true",
+      endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
+      toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
       moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
       projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
       editable: can_edit_issuable,
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 274b38a7708cfc98f3fd152d93f41998c693e7a8..f478c8ede18501f700f14c9a7bb82fd10ee099a0 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -13,6 +13,8 @@ module Subscribable
   end
 
   def subscribed?(user, project = nil)
+    return false unless user
+
     if subscription = subscriptions.find_by(user: user, project: project)
       subscription.subscribed
     else
diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ff23d8bf0c703511dd03548be3155f4d3c8a11df
--- /dev/null
+++ b/app/serializers/issuable_sidebar_entity.rb
@@ -0,0 +1,16 @@
+class IssuableSidebarEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :participants, using: ::API::Entities::UserBasic do |issuable|
+    issuable.participants(request.current_user)
+  end
+
+  expose :subscribed do |issuable|
+    issuable.subscribed?(request.current_user, issuable.project)
+  end
+
+  expose :time_estimate
+  expose :total_time_spent
+  expose :human_time_estimate
+  expose :human_total_time_spent
+end
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index 4fff54a91265b9b81c5881b332ffac7340fbf906..2555595379b553b76d53ad03dddbbaf215809b82 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -1,3 +1,16 @@
 class IssueSerializer < BaseSerializer
-  entity IssueEntity
+  # This overrided method takes care of which entity should be used
+  # to serialize the `issue` based on `basic` key in `opts` param.
+  # Hence, `entity` doesn't need to be declared on the class scope.
+  def represent(merge_request, opts = {})
+    entity =
+      case opts[:serializer]
+      when 'sidebar'
+        IssueSidebarEntity
+      else
+        IssueEntity
+      end
+
+    super(merge_request, opts, entity)
+  end
 end
diff --git a/app/serializers/issue_sidebar_entity.rb b/app/serializers/issue_sidebar_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6c823dbfe951b2b5f80dc2671b5378bde7d72d8d
--- /dev/null
+++ b/app/serializers/issue_sidebar_entity.rb
@@ -0,0 +1,3 @@
+class IssueSidebarEntity < IssuableSidebarEntity
+  expose :assignees, using: API::Entities::UserBasic
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 8461f158bb505bd36353d3c4011c367f76135f5e..d54a6516aedf3733fe1f3af848c2f29a40780c20 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,11 +1,7 @@
-class MergeRequestBasicEntity < Grape::Entity
+class MergeRequestBasicEntity < IssuableSidebarEntity
   expose :assignee_id
   expose :merge_status
   expose :merge_error
   expose :state
   expose :source_branch_exists?, as: :source_branch_exists
-  expose :time_estimate
-  expose :total_time_spent
-  expose :human_time_estimate
-  expose :human_total_time_spent
 end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index f67034ce47adbbc155b0058b87239b41224c3bcc..e9d98d8baca79125bd466a6f3b49b21135cc9a0e 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer
   # to serialize the `merge_request` based on `basic` key in `opts` param.
   # Hence, `entity` doesn't need to be declared on the class scope.
   def represent(merge_request, opts = {})
-    entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity
+    entity =
+      case opts[:serializer]
+      when 'basic', 'sidebar'
+        MergeRequestBasicEntity
+      else
+        MergeRequestEntity
+      end
+
     super(merge_request, opts, entity)
   end
 end
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
deleted file mode 100644
index 3f553c9fede46754e8e4d0efeea56b9f504f9ca2..0000000000000000000000000000000000000000
--- a/app/views/shared/issuable/_participants.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- participants_row = 7
-- participants_size = participants.size
-- participants_extra = participants_size - participants_row
-.block.participants
-  .sidebar-collapsed-icon
-    = icon('users')
-    %span
-      = participants.count
-  .title.hide-collapsed
-    = pluralize participants.count, "participant"
-  .hide-collapsed.participants-list
-    - participants.each do |participant|
-      .participants-author.js-participants-author
-        = link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
-  - if participants_extra > 0
-    .hide-collapsed.participants-more
-      %button.btn-transparent.btn-blank.js-participants-more{ type: 'button', data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
-        + #{participants_extra} more
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7b7411b1e234ba67000773098441778523f2f1d4..e0009a35b9fce0778f9e6f1f190f8902619e03e8 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -123,17 +123,10 @@
         %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
         #js-lock-entry-point
 
-      = render "shared/issuable/participants", participants: issuable.participants(current_user)
+      .js-sidebar-participants-entry-point
+
       - if current_user
-        - subscribed = issuable.subscribed?(current_user, @project)
-        .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
-          .sidebar-collapsed-icon
-            = icon('rss', 'aria-hidden': 'true')
-          %span.issuable-header-text.hide-collapsed.pull-left
-            Notifications
-          - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
-          %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
-            %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+        .js-sidebar-subscriptions-entry-point
 
       - project_ref = cross_project_reference(@project, issuable)
       .block.project-reference
diff --git a/changelogs/unreleased/23206-load-participants-async.yml b/changelogs/unreleased/23206-load-participants-async.yml
new file mode 100644
index 0000000000000000000000000000000000000000..12ab43fb88f322f51fe388a5cd296cda54e60bac
--- /dev/null
+++ b/changelogs/unreleased/23206-load-participants-async.yml
@@ -0,0 +1,5 @@
+---
+title: Update participants and subscriptions button in issuable sidebar to be async
+merge_request: 14836
+author:
+type: changed
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 2c3ef2efd52d880937af49065189cae6bf47dbfd..3843374678c8133fc012ff31bb27b4c74faaf766 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
   end
 
   step 'I should see that I am subscribed' do
-    expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
+    wait_for_requests
+    expect(find('.js-issuable-subscribe-button span')).to have_content 'Unsubscribe'
   end
 
   step 'I should see that I am unsubscribed' do
-    expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
+    wait_for_requests
+    expect(find('.js-issuable-subscribe-button span')).to have_content 'Subscribe'
   end
 
   step 'I click link "Closed"' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index b7cccddefdd6f44bd3670b858d61ecbf41617b89..52ef8c6a589feddedac2685ec88a55ab1b0adf8c 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do
     end
 
     describe 'as json' do
-      context 'with basic param' do
+      context 'with basic serializer param' do
         it 'renders basic MR entity as json' do
-          go(basic: true, format: :json)
+          go(serializer: 'basic', format: :json)
 
           expect(response).to match_response_schema('entities/merge_request_basic')
         end
       end
 
-      context 'without basic param' do
+      context 'without basic serializer param' do
         it 'renders the merge request in the json format' do
           go(format: :json)
 
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 60ed17c0c81af679b177f79d98c9822fc5df8454..ebe6939df4c4d20476694fca9a33c4cf7a3b696c 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -538,7 +538,7 @@ describe 'Issue Boards', :js do
     end
 
     it 'does not show create new list' do
-      expect(page).not_to have_selector('.js-new-board-list')
+      expect(page).not_to have_button('.js-new-board-list')
     end
 
     it 'does not allow dragging' do
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
index 30a80f8e652ec1d3ac1776040a7a8f06fbb4cb55..4ca435491cb5069e43398f57521fb416cc2e02ae 100644
--- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do
   end
 
   it 'toggles subscription' do
-    subscribe_button = find('.issuable-subscribe-button span')
+    subscribe_button = find('.js-issuable-subscribe-button')
 
     expect(subscribe_button).to have_content('Subscribe')
 
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
new file mode 100644
index 0000000000000000000000000000000000000000..3d3329a340645394af84112542dcbae2ea931a8f
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -0,0 +1,44 @@
+{
+  "type": "object",
+  "properties" : {
+    "id": { "type": "integer" },
+    "iid": { "type": "integer" },
+    "author_id": { "type": "integer" },
+    "description": { "type": ["string", "null"] },
+    "lock_version": { "type": ["string", "null"] },
+    "milestone_id": { "type": ["string", "null"] },
+    "title": { "type": "string" },
+    "moved_to_id": { "type": ["integer", "null"] },
+    "project_id": { "type": "integer" },
+    "web_url": { "type": "string" },
+    "state": { "type": "string" },
+    "create_note_path": { "type": "string" },
+    "preview_note_path": { "type": "string" },
+    "current_user": {
+      "type": "object",
+      "properties": {
+        "can_create_note": { "type": "boolean" },
+        "can_update": { "type": "boolean" }
+      }
+    },
+    "created_at": { "type": "date-time" },
+    "updated_at": { "type": "date-time" },
+    "branch_name": { "type": ["string", "null"] },
+    "due_date": { "type": "date" },
+    "confidential": { "type": "boolean" },
+    "discussion_locked": { "type": ["boolean", "null"] },
+    "updated_by_id": { "type": ["string", "null"] },
+    "deleted_at": { "type": ["string", "null"] },
+    "time_estimate": { "type": "integer" },
+    "total_time_spent": { "type": "integer" },
+    "human_time_estimate": { "type": ["integer", "null"] },
+    "human_total_time_spent": { "type": ["integer", "null"] },
+    "milestone": { "type": ["object", "null"] },
+    "labels": {
+      "type": "array",
+      "items": { "$ref": "label.json" }
+    },
+    "assignees": { "type": ["array", "null"] }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/issue_sidebar.json b/spec/fixtures/api/schemas/entities/issue_sidebar.json
new file mode 100644
index 0000000000000000000000000000000000000000..682e345d5f58a6d95e9e62e669011ff00cf3e3e1
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue_sidebar.json
@@ -0,0 +1,21 @@
+{
+  "type": "object",
+  "properties" : {
+    "id": { "type": "integer" },
+    "iid": { "type": "integer" },
+    "subscribed": { "type": "boolean" },
+    "time_estimate": { "type": "integer" },
+    "total_time_spent": { "type": "integer" },
+    "human_time_estimate": { "type": ["integer", "null"] },
+    "human_total_time_spent": { "type": ["integer", "null"] },
+    "participants": {
+      "type": "array",
+      "items": { "$ref": "../public_api/v4/user/basic.json" }
+    },
+    "assignees": {
+      "type": "array",
+      "items": { "$ref": "../public_api/v4/user/basic.json" }
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/label.json b/spec/fixtures/api/schemas/entities/label.json
new file mode 100644
index 0000000000000000000000000000000000000000..40dff764c1720d2bce404e6efbffe21503bab9eb
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/label.json
@@ -0,0 +1,26 @@
+{
+  "type": "object",
+  "required": [
+    "id",
+    "color",
+    "description",
+    "title",
+    "priority"
+  ],
+  "properties": {
+    "id": { "type": "integer" },
+    "color": {
+      "type": "string",
+      "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+    },
+    "description": { "type": ["string", "null"] },
+    "text_color": {
+      "type": "string",
+      "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+    },
+    "type": { "type": "string" },
+    "title": { "type": "string" },
+    "priority": { "type": ["integer", "null"] }
+  },
+  "additionalProperties": false
+}
\ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 6b14188582a69f842bc45b34e46501fec37252d7..995f13381adb36fbda15778ad4ed7881e5fda6dd 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -9,7 +9,9 @@
     "human_time_estimate": { "type": ["string", "null"] },
     "human_total_time_spent": { "type": ["string", "null"] },
     "merge_error": { "type": ["string", "null"] },
-    "assignee_id": { "type": ["integer", "null"] }
+    "assignee_id": { "type": ["integer", "null"] },
+    "subscribed": { "type": ["boolean", "null"] },
+    "participants": { "type": "array" }
   },
   "additionalProperties": false
 }
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index e1f62508933ad9cb72cc63a381be476e06c4a94b..a55ecaa5697471c845cce7b6936ed8f57234cc66 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -19,32 +19,7 @@
     },
     "labels": {
       "type": "array",
-      "items": {
-        "type": "object",
-        "required": [
-          "id",
-          "color",
-          "description",
-          "title",
-          "priority"
-        ],
-        "properties": {
-          "id": { "type": "integer" },
-          "color": {
-            "type": "string",
-            "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
-          },
-          "description": { "type": ["string", "null"] },
-          "text_color": {
-            "type": "string",
-            "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
-          },
-          "type": { "type": "string" },
-          "title": { "type": "string" },
-          "priority": { "type": ["integer", "null"] }
-        },
-        "additionalProperties": false
-      }
+      "items": { "$ref": "entities/label.json" }
     },
     "assignee": {
       "id": { "type": "integet" },
diff --git a/spec/javascripts/issuable_context_spec.js b/spec/javascripts/issuable_context_spec.js
deleted file mode 100644
index f266209027a2331fb60b0bed10ea5dc344bdbb32..0000000000000000000000000000000000000000
--- a/spec/javascripts/issuable_context_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'jquery';
-import IssuableContext from '~/issuable_context';
-
-describe('IssuableContext', () => {
-  describe('toggleHiddenParticipants', () => {
-    const event = jasmine.createSpyObj('event', ['preventDefault']);
-
-    beforeEach(() => {
-      spyOn($.fn, 'data').and.returnValue('data');
-      spyOn($.fn, 'text').and.returnValue('data');
-    });
-
-    afterEach(() => {
-      gl.lazyLoader = undefined;
-    });
-
-    it('calls loadCheck if lazyLoader is set', () => {
-      gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
-
-      IssuableContext.prototype.toggleHiddenParticipants(event);
-
-      expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
-    });
-
-    it('does not throw if lazyLoader is not defined', () => {
-      gl.lazyLoader = undefined;
-
-      const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event);
-
-      expect(toggle).not.toThrow();
-    });
-  });
-});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index e2b6bcabc984e915fb948ad1fa9949233d9fb70d..0682b463043a289a5bf2fe1b678dd88b7534f2de 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -109,12 +109,14 @@ const sidebarMockData = {
       labels: [],
       web_url: '/root/some-project/issues/5',
     },
+    '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
   },
 };
 
 export default {
   mediator: {
     endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+    toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
     moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
     projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
     editable: true,
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..30cc549c7c01f7242398efd51fc3fd875bb9395e
--- /dev/null
+++ b/spec/javascripts/sidebar/participants_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import participants from '~/sidebar/components/participants/participants.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+const PARTICIPANT = {
+  id: 1,
+  state: 'active',
+  username: 'marcene',
+  name: 'Allie Will',
+  web_url: 'foo.com',
+  avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+  PARTICIPANT,
+  { ...PARTICIPANT, id: 2 },
+  { ...PARTICIPANT, id: 3 },
+];
+
+describe('Participants', function () {
+  let vm;
+  let Participants;
+
+  beforeEach(() => {
+    Participants = Vue.extend(participants);
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+  });
+
+  describe('collapsed sidebar state', () => {
+    it('shows loading spinner when loading', () => {
+      vm = mountComponent(Participants, {
+        loading: true,
+      });
+
+      expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
+    });
+
+    it('shows participant count when given', () => {
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+      });
+      const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+      expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+    });
+
+    it('shows full participant count when there are hidden participants', () => {
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants: 1,
+      });
+      const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+      expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+    });
+  });
+
+  describe('expanded sidebar state', () => {
+    it('shows loading spinner when loading', () => {
+      vm = mountComponent(Participants, {
+        loading: true,
+      });
+
+      expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
+    });
+
+    it('when only showing visible participants, shows an avatar only for each participant under the limit', (done) => {
+      const numberOfLessParticipants = 2;
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants,
+      });
+      vm.isShowingMoreParticipants = false;
+
+      Vue.nextTick()
+        .then(() => {
+          const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+          expect(participantEls.length).toBe(numberOfLessParticipants);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('when only showing all participants, each has an avatar', (done) => {
+      const numberOfLessParticipants = 2;
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants,
+      });
+      vm.isShowingMoreParticipants = true;
+
+      Vue.nextTick()
+        .then(() => {
+          const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+          expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('does not have more participants link when they can all be shown', () => {
+      const numberOfLessParticipants = 100;
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants,
+      });
+      const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+      expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+      expect(moreParticipantLink).toBeNull();
+    });
+
+    it('when too many participants, has more participants link to show more', (done) => {
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants: 2,
+      });
+      vm.isShowingMoreParticipants = false;
+
+      Vue.nextTick()
+        .then(() => {
+          const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+          expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('when too many participants and already showing them, has more participants link to show less', (done) => {
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants: 2,
+      });
+      vm.isShowingMoreParticipants = true;
+
+      Vue.nextTick()
+        .then(() => {
+          const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+          expect(moreParticipantLink.textContent.trim()).toBe('- show less');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('clicking more participants link emits event', () => {
+      vm = mountComponent(Participants, {
+        loading: false,
+        participants: PARTICIPANT_LIST,
+        numberOfLessParticipants: 2,
+      });
+      const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+      expect(vm.isShowingMoreParticipants).toBe(false);
+
+      moreParticipantLink.click();
+
+      expect(vm.isShowingMoreParticipants).toBe(true);
+    });
+  });
+});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 3aa8ca5db0d7c8699dafb225f73eb17cb4e9a4a1..7deb1fd21189a0a89e4968fac98a7c3cae5c13a0 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => {
       .then(() => {
         expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
         expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
-        done();
       })
+      .then(done)
       .catch(done.fail);
   });
 
@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => {
       .then(() => {
         expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
         expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
-        done();
       })
+      .then(done)
+      .catch(done.fail);
+  });
+
+  it('toggle subscription', (done) => {
+    this.mediator.store.setSubscribedState(false);
+    spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
+
+    this.mediator.toggleSubscription()
+      .then(() => {
+        expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
+        expect(this.mediator.store.subscribed).toEqual(true);
+      })
+      .then(done)
       .catch(done.fail);
   });
 });
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
index a4bd8ba8d88a8ffb9c977db51065404b4d69f549..7324d34d84ad23d25c7535a4c0feef8c2b0bf7fa 100644
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ b/spec/javascripts/sidebar/sidebar_service_spec.js
@@ -7,6 +7,7 @@ describe('Sidebar service', () => {
     Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
     this.service = new SidebarService({
       endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+      toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
       moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
       projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
     });
@@ -23,6 +24,7 @@ describe('Sidebar service', () => {
         expect(resp).toBeDefined();
         done();
       })
+      .then(done)
       .catch(done.fail);
   });
 
@@ -30,8 +32,8 @@ describe('Sidebar service', () => {
     this.service.update('issue[assignee_ids]', [1])
       .then((resp) => {
         expect(resp).toBeDefined();
-        done();
       })
+      .then(done)
       .catch(done.fail);
   });
 
@@ -39,8 +41,8 @@ describe('Sidebar service', () => {
     this.service.getProjectsAutocomplete()
       .then((resp) => {
         expect(resp).toBeDefined();
-        done();
       })
+      .then(done)
       .catch(done.fail);
   });
 
@@ -48,8 +50,17 @@ describe('Sidebar service', () => {
     this.service.moveIssue(123)
       .then((resp) => {
         expect(resp).toBeDefined();
-        done();
       })
+      .then(done)
+      .catch(done.fail);
+  });
+
+  it('toggles the subscription', (done) => {
+    this.service.toggleSubscription()
+      .then((resp) => {
+        expect(resp).toBeDefined();
+      })
+      .then(done)
       .catch(done.fail);
   });
 });
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 69eb3839d675e1cd0c5536b9413da22c0e737de4..51dee64fb9311f36f2159e72fd57563b42ff1a9a 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store';
 import Mock from './mock_data';
 import UsersMockHelper from '../helpers/user_mock_data_helper';
 
-describe('Sidebar store', () => {
-  const assignee = {
-    id: 2,
-    name: 'gitlab user 2',
-    username: 'gitlab2',
-    avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
-  };
-
-  const anotherAssignee = {
-    id: 3,
-    name: 'gitlab user 3',
-    username: 'gitlab3',
-    avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
-  };
+const ASSIGNEE = {
+  id: 2,
+  name: 'gitlab user 2',
+  username: 'gitlab2',
+  avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const ANOTHER_ASSINEE = {
+  id: 3,
+  name: 'gitlab user 3',
+  username: 'gitlab3',
+  avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const PARTICIPANT = {
+  id: 1,
+  state: 'active',
+  username: 'marcene',
+  name: 'Allie Will',
+  web_url: 'foo.com',
+  avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+  PARTICIPANT,
+  { ...PARTICIPANT, id: 2 },
+  { ...PARTICIPANT, id: 3 },
+];
 
+describe('Sidebar store', () => {
   beforeEach(() => {
     this.store = new SidebarStore({
       currentUser: {
@@ -40,23 +55,23 @@ describe('Sidebar store', () => {
   });
 
   it('adds a new assignee', () => {
-    this.store.addAssignee(assignee);
+    this.store.addAssignee(ASSIGNEE);
     expect(this.store.assignees.length).toEqual(1);
   });
 
   it('removes an assignee', () => {
-    this.store.removeAssignee(assignee);
+    this.store.removeAssignee(ASSIGNEE);
     expect(this.store.assignees.length).toEqual(0);
   });
 
   it('finds an existent assignee', () => {
     let foundAssignee;
 
-    this.store.addAssignee(assignee);
-    foundAssignee = this.store.findAssignee(assignee);
+    this.store.addAssignee(ASSIGNEE);
+    foundAssignee = this.store.findAssignee(ASSIGNEE);
     expect(foundAssignee).toBeDefined();
-    expect(foundAssignee).toEqual(assignee);
-    foundAssignee = this.store.findAssignee(anotherAssignee);
+    expect(foundAssignee).toEqual(ASSIGNEE);
+    foundAssignee = this.store.findAssignee(ANOTHER_ASSINEE);
     expect(foundAssignee).toBeUndefined();
   });
 
@@ -65,6 +80,28 @@ describe('Sidebar store', () => {
     expect(this.store.assignees.length).toEqual(0);
   });
 
+  it('sets participants data', () => {
+    expect(this.store.participants.length).toEqual(0);
+
+    this.store.setParticipantsData({
+      participants: PARTICIPANT_LIST,
+    });
+
+    expect(this.store.isFetching.participants).toEqual(false);
+    expect(this.store.participants.length).toEqual(PARTICIPANT_LIST.length);
+  });
+
+  it('sets subcriptions data', () => {
+    expect(this.store.subscribed).toEqual(null);
+
+    this.store.setSubscriptionsData({
+      subscribed: true,
+    });
+
+    expect(this.store.isFetching.subscriptions).toEqual(false);
+    expect(this.store.subscribed).toEqual(true);
+  });
+
   it('set assigned data', () => {
     const users = {
       assignees: UsersMockHelper.createNumberRandomUsers(3),
@@ -75,6 +112,14 @@ describe('Sidebar store', () => {
     expect(this.store.assignees.length).toEqual(3);
   });
 
+  it('sets fetching state', () => {
+    expect(this.store.isFetching.participants).toEqual(true);
+
+    this.store.setFetchingState('participants', false);
+
+    expect(this.store.isFetching.participants).toEqual(false);
+  });
+
   it('set time tracking data', () => {
     this.store.setTimeTrackingData(Mock.time);
     expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
@@ -90,6 +135,14 @@ describe('Sidebar store', () => {
     expect(this.store.autocompleteProjects).toEqual(projects);
   });
 
+  it('sets subscribed state', () => {
+    expect(this.store.subscribed).toEqual(null);
+
+    this.store.setSubscribedState(true);
+
+    expect(this.store.subscribed).toEqual(true);
+  });
+
   it('set move to project ID', () => {
     const projectId = 7;
     this.store.setMoveToProjectId(projectId);
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7adf22b0f1fec51e5b84c8f6641083eb5bb39f25
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import eventHub from '~/sidebar/event_hub';
+import mountComponent from '../helpers/vue_mount_component_helper';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', function () {
+  let vm;
+  let SidebarSubscriptions;
+
+  beforeEach(() => {
+    SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
+    // Setup the stores, services, etc
+    // eslint-disable-next-line no-new
+    new SidebarMediator(Mock.mediator);
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+    SidebarService.singleton = null;
+    SidebarStore.singleton = null;
+    SidebarMediator.singleton = null;
+  });
+
+  it('calls the mediator toggleSubscription on event', () => {
+    spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
+    vm = mountComponent(SidebarSubscriptions, {});
+
+    eventHub.$emit('toggleSubscription');
+
+    expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
+  });
+});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b33dd02fb99f1679844a257f9c500068b26b6b6
--- /dev/null
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Subscriptions', function () {
+  let vm;
+  let Subscriptions;
+
+  beforeEach(() => {
+    Subscriptions = Vue.extend(subscriptions);
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+  });
+
+  it('shows loading spinner when loading', () => {
+    vm = mountComponent(Subscriptions, {
+      loading: true,
+      subscribed: undefined,
+    });
+
+    expect(vm.$refs.loadingButton.loading).toBe(true);
+    expect(vm.$refs.loadingButton.label).toBeUndefined();
+  });
+
+  it('has "Subscribe" text when currently not subscribed', () => {
+    vm = mountComponent(Subscriptions, {
+      subscribed: false,
+    });
+
+    expect(vm.$refs.loadingButton.label).toBe('Subscribe');
+  });
+
+  it('has "Unsubscribe" text when currently not subscribed', () => {
+    vm = mountComponent(Subscriptions, {
+      subscribed: true,
+    });
+
+    expect(vm.$refs.loadingButton.label).toBe('Unsubscribe');
+  });
+});
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index 28ff8158e0e24b12515e328305b9467eb19a8db4..45dfb136aea795fc270e66e40bfbb75861a26653 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do
   let(:user_1)   { create(:user) }
 
   describe '#subscribed?' do
+    context 'without user' do
+      it 'returns false' do
+        expect(resource.subscribed?(nil, project)).to be_falsey
+      end
+    end
+
     context 'without project' do
       it 'returns false when no subscription exists' do
         expect(resource.subscribed?(user_1)).to be_falsey
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..75578816e753f1ace22a98e0158b7437d637d81d
--- /dev/null
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe IssueSerializer do
+  let(:resource) { create(:issue) }
+  let(:user)     { create(:user) }
+  let(:json_entity) do
+    described_class.new(current_user: user)
+      .represent(resource, serializer: serializer)
+      .with_indifferent_access
+  end
+
+  context 'non-sidebar issue serialization' do
+    let(:serializer) { nil }
+
+    it 'matches issue json schema' do
+      expect(json_entity).to match_schema('entities/issue')
+    end
+  end
+
+  context 'sidebar issue serialization' do
+    let(:serializer) { 'sidebar' }
+
+    it 'matches sidebar issue json schema' do
+      expect(json_entity).to match_schema('entities/issue_sidebar')
+    end
+  end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
index 4daf5a59d0ccf9498a6e49cd3ca608d50f6c8628..1fad8e6bc5daaaf9dfbdd62462b707d313646671 100644
--- a/spec/serializers/merge_request_basic_serializer_spec.rb
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do
   let(:resource) { create(:merge_request) }
   let(:user)     { create(:user) }
 
-  subject { described_class.new.represent(resource) }
+  let(:json_entity) do
+    described_class.new(current_user: user)
+      .represent(resource, serializer: 'basic')
+      .with_indifferent_access
+  end
 
-  it 'has important MergeRequest attributes' do
-    expect(subject).to include(:merge_status)
+  it 'matches basic merge request json' do
+    expect(json_entity).to match_schema('entities/merge_request_basic')
   end
 end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index 73fbecc153d76b03809c7238be52246107898a7a..e3abefa6d63c2977950ab6446d19d70a9ca72525 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -9,11 +9,11 @@ describe MergeRequestSerializer do
   end
 
   describe '#represent' do
-    let(:opts) { { basic: basic } }
-    subject { serializer.represent(merge_request, basic: basic) }
+    let(:opts) { { serializer: serializer_entity } }
+    subject { serializer.represent(merge_request, serializer: serializer_entity) }
 
-    context 'when basic param is truthy' do
-      let(:basic) { true }
+    context 'when passing basic serializer param' do
+      let(:serializer_entity) { 'basic' }
 
       it 'calls super class #represent with correct params' do
         expect_any_instance_of(BaseSerializer).to receive(:represent)
@@ -23,8 +23,8 @@ describe MergeRequestSerializer do
       end
     end
 
-    context 'when basic param is falsy' do
-      let(:basic) { false }
+    context 'when serializer param is falsy' do
+      let(:serializer_entity) { nil }
 
       it 'calls super class #represent with correct params' do
         expect_any_instance_of(BaseSerializer).to receive(:represent)
diff --git a/spec/views/shared/issuable/_participants.html.haml.rb b/spec/views/shared/issuable/_participants.html.haml.rb
deleted file mode 100644
index 51059d4c0d7d2f6b0c32832406a4882bb1fa5b14..0000000000000000000000000000000000000000
--- a/spec/views/shared/issuable/_participants.html.haml.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-require 'nokogiri'
-
-describe 'shared/issuable/_participants.html.haml' do
-  let(:project) { create(:project) }
-  let(:participants)  { create_list(:user, 100) }
-
-  before do
-    allow(view).to receive_messages(project: project,
-                                    participants: participants)
-  end
-
-  it 'renders lazy loaded avatars' do
-    render 'shared/issuable/participants'
-
-    html = Nokogiri::HTML(rendered)
-
-    avatars = html.css('.participants-author img')
-
-    avatars.each do |avatar|
-      expect(avatar[:class]).to include('lazy')
-      expect(avatar[:src]).to eql(LazyImageTagHelper.placeholder_image)
-      expect(avatar[:"data-src"]).to match('http://www.gravatar.com/avatar/')
-    end
-  end
-end