diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 2bcff541cc0967271ae81cf04aedc661d2cd892f..5eb1bd86e9db5380ee1090a48a7ae19ca8ef4658 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service
     return unless valid_token?(params[:token])
 
     user = find_chat_user(params)
-    unless user
+
+    if user
+      Gitlab::ChatCommands::Command.new(project, user, params).execute
+    else
       url = authorize_chat_name_url(params)
-      return presenter.authorize_chat_name(url)
+      Gitlab::ChatCommands::Presenters::Access.new(url).authorize
     end
-
-    Gitlab::ChatCommands::Command.new(project, user,
-      params).execute
   end
 
   private
@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service
   def authorize_chat_name_url(params)
     ChatNames::AuthorizeUserService.new(self, params).execute
   end
-
-  def presenter
-    Gitlab::ChatCommands::Presenter.new
-  end
 end
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2494884f5c92a5bad9b1f7b88f992dcf52194c04
--- /dev/null
+++ b/changelogs/unreleased/zj-format-chat-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Reformat messages ChatOps
+merge_request: 8528
+author:
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 4fe53ce93a99791d3be0a9e8fea635e81db5c451..25da8474e95a0f7c43c507b3f18be238ef849c63 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,10 +42,6 @@ module Gitlab
       def find_by_iid(iid)
         collection.find_by(iid: iid)
       end
-
-      def presenter
-        Gitlab::ChatCommands::Presenter.new
-      end
     end
   end
 end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index 145086755e4a2240ef123b9097fecd88d75fdc6a..f34ed0f4cf29c3574b78720a1f3530b8e80f9aa6 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -3,7 +3,7 @@ module Gitlab
     class Command < BaseCommand
       COMMANDS = [
         Gitlab::ChatCommands::IssueShow,
-        Gitlab::ChatCommands::IssueCreate,
+        Gitlab::ChatCommands::IssueNew,
         Gitlab::ChatCommands::IssueSearch,
         Gitlab::ChatCommands::Deploy,
       ].freeze
@@ -13,51 +13,32 @@ module Gitlab
 
         if command
           if command.allowed?(project, current_user)
-            present command.new(project, current_user, params).execute(match)
+            command.new(project, current_user, params).execute(match)
           else
-            access_denied
+            Gitlab::ChatCommands::Presenters::Access.new.access_denied
           end
         else
-          help(help_messages)
+          Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
         end
       end
 
       def match_command
         match = nil
-        service = available_commands.find do |klass|
-          match = klass.match(command)
-        end
+        service =
+          available_commands.find do |klass|
+            match = klass.match(params[:text])
+          end
 
         [service, match]
       end
 
       private
 
-      def help_messages
-        available_commands.map(&:help_message)
-      end
-
       def available_commands
         COMMANDS.select do |klass|
           klass.available?(project)
         end
       end
-
-      def command
-        params[:text]
-      end
-
-      def help(messages)
-        presenter.help(messages, params[:command])
-      end
-
-      def access_denied
-        presenter.access_denied
-      end
-
-      def present(resource)
-        presenter.present(resource)
-      end
     end
   end
 end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 7127d2f6d047cac40f779fb609c3e20521b417d2..458d90f84e8f066535b4b2790f92d82e7cd73b2c 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -1,8 +1,6 @@
 module Gitlab
   module ChatCommands
     class Deploy < BaseCommand
-      include Gitlab::Routing.url_helpers
-
       def self.match(text)
         /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
       end
@@ -24,35 +22,29 @@ module Gitlab
         to = match[:to]
 
         actions = find_actions(from, to)
-        return unless actions.present?
 
-        if actions.one?
-          play!(from, to, actions.first)
+        if actions.none?
+          Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
+        elsif actions.one?
+          action = play!(from, to, actions.first)
+          Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
         else
-          Result.new(:error, 'Too many actions defined')
+          Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
         end
       end
 
       private
 
       def play!(from, to, action)
-        new_action = action.play(current_user)
-
-        Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
+        action.play(current_user)
       end
 
       def find_actions(from, to)
         environment = project.environments.find_by(name: from)
-        return unless environment
+        return [] unless environment
 
         environment.actions_for(to).select(&:starts_environment?)
       end
-
-      def url(subject)
-        polymorphic_url(
-          [subject.project.namespace.becomes(Namespace), subject.project, subject]
-        )
-      end
     end
   end
 end
diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6c0e4d304a4f79f6f9943b8d61a3dd953895512d
--- /dev/null
+++ b/lib/gitlab/chat_commands/help.rb
@@ -0,0 +1,28 @@
+module Gitlab
+  module ChatCommands
+    class Help < BaseCommand
+      # This class has to be used last, as it always matches. It has to match
+      # because other commands were not triggered and we want to show the help
+      # command
+      def self.match(_text)
+        true
+      end
+
+      def self.help_message
+        'help'
+      end
+
+      def self.allowed?(_project, _user)
+        true
+      end
+
+      def execute(commands, text)
+        Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text)
+      end
+
+      def trigger
+        params[:command]
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_new.rb
similarity index 63%
rename from lib/gitlab/chat_commands/issue_create.rb
rename to lib/gitlab/chat_commands/issue_new.rb
index cefb6775db854becff484cc09f75073489a1611f..016054ecd465e73aea59d3954663c20a67a11678 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_new.rb
@@ -1,8 +1,8 @@
 module Gitlab
   module ChatCommands
-    class IssueCreate < IssueCommand
+    class IssueNew < IssueCommand
       def self.match(text)
-        # we can not match \n with the dot by passing the m modifier as than 
+        # we can not match \n with the dot by passing the m modifier as than
         # the title and description are not seperated
         /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
       end
@@ -19,8 +19,24 @@ module Gitlab
         title = match[:title]
         description = match[:description].to_s.rstrip
 
+        issue = create_issue(title: title, description: description)
+
+        if issue.persisted?
+          presenter(issue).present
+        else
+          presenter(issue).display_errors
+        end
+      end
+
+      private
+
+      def create_issue(title:, description:)
         Issues::CreateService.new(project, current_user, title: title, description: description).execute
       end
+
+      def presenter(issue)
+        Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
+      end
     end
   end
 end
diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb
index 51bf80c800b73f4fe09292859b8f68e91b16ac29..3491b53093ecf6db66fd9f19483520b213bfd51c 100644
--- a/lib/gitlab/chat_commands/issue_search.rb
+++ b/lib/gitlab/chat_commands/issue_search.rb
@@ -10,7 +10,13 @@ module Gitlab
       end
 
       def execute(match)
-        collection.search(match[:query]).limit(QUERY_LIMIT)
+        issues = collection.search(match[:query]).limit(QUERY_LIMIT)
+
+        if issues.present?
+          Presenters::IssueSearch.new(issues).present
+        else
+          Presenters::Access.new(issues).not_found
+        end
       end
     end
   end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
index 2a45d49cf6b30824f5672ccfcebb1e9172ced818..d6013f4d10cb0fdef8deb5b29bb0b46b8ddecffa 100644
--- a/lib/gitlab/chat_commands/issue_show.rb
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -10,7 +10,13 @@ module Gitlab
       end
 
       def execute(match)
-        find_by_iid(match[:iid])
+        issue = find_by_iid(match[:iid])
+
+        if issue
+          Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present
+        else
+          Gitlab::ChatCommands::Presenters::Access.new.not_found
+        end
       end
     end
   end
diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
deleted file mode 100644
index 8930a21f4065b231029d2ee986a2676211413c60..0000000000000000000000000000000000000000
--- a/lib/gitlab/chat_commands/presenter.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-module Gitlab
-  module ChatCommands
-    class Presenter
-      include Gitlab::Routing
-
-      def authorize_chat_name(url)
-        message = if url
-                    ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
-                  else
-                    ":sweat_smile: Couldn't identify you, nor can I autorize you!"
-                  end
-
-        ephemeral_response(message)
-      end
-
-      def help(commands, trigger)
-        if commands.none?
-          ephemeral_response("No commands configured")
-        else
-          commands.map! { |command| "#{trigger} #{command}" }
-          message = header_with_list("Available commands", commands)
-
-          ephemeral_response(message)
-        end
-      end
-
-      def present(subject)
-        return not_found unless subject
-
-        if subject.is_a?(Gitlab::ChatCommands::Result)
-          show_result(subject)
-        elsif subject.respond_to?(:count)
-          if subject.none?
-            not_found
-          elsif subject.one?
-            single_resource(subject.first)
-          else
-            multiple_resources(subject)
-          end
-        else
-          single_resource(subject)
-        end
-      end
-
-      def access_denied
-        ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
-      end
-
-      private
-
-      def show_result(result)
-        case result.type
-        when :success
-          in_channel_response(result.message)
-        else
-          ephemeral_response(result.message)
-        end
-      end
-
-      def not_found
-        ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
-      end
-
-      def single_resource(resource)
-        return error(resource) if resource.errors.any? || !resource.persisted?
-
-        message = "#{title(resource)}:"
-        message << "\n\n#{resource.description}" if resource.try(:description)
-
-        in_channel_response(message)
-      end
-
-      def multiple_resources(resources)
-        titles = resources.map { |resource| title(resource) }
-
-        message = header_with_list("Multiple results were found:", titles)
-
-        ephemeral_response(message)
-      end
-
-      def error(resource)
-        message = header_with_list("The action was not successful, because:", resource.errors.messages)
-
-        ephemeral_response(message)
-      end
-
-      def title(resource)
-        reference = resource.try(:to_reference) || resource.try(:id)
-        title = resource.try(:title) || resource.try(:name)
-
-        "[#{reference} #{title}](#{url(resource)})"
-      end
-
-      def header_with_list(header, items)
-        message = [header]
-
-        items.each do |item|
-          message << "- #{item}"
-        end
-
-        message.join("\n")
-      end
-
-      def url(resource)
-        url_for(
-          [
-            resource.project.namespace.becomes(Namespace),
-            resource.project,
-            resource
-          ]
-        )
-      end
-
-      def ephemeral_response(message)
-        {
-          response_type: :ephemeral,
-          text: message,
-          status: 200
-        }
-      end
-
-      def in_channel_response(message)
-        {
-          response_type: :in_channel,
-          text: message,
-          status: 200
-        }
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb
new file mode 100644
index 0000000000000000000000000000000000000000..92f4fa17f78d32a66b04a9bd116382304a11c1c3
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/access.rb
@@ -0,0 +1,40 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class Access < Presenters::Base
+        def access_denied
+          ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
+        end
+
+        def not_found
+          ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
+        end
+
+        def authorize
+          message =
+            if @resource
+              ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
+            else
+              ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+            end
+
+          ephemeral_response(text: message)
+        end
+
+        def unknown_command(commands)
+          ephemeral_response(text: help_message(trigger))
+        end
+
+        private
+
+        def help_message(trigger)
+          header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
+        end
+
+        def full_commands(trigger)
+          @resource.map { |command| "#{trigger} #{command.help_message}" }
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2700a5a2ad5004e6d09595353882d5c73d4d1a3e
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/base.rb
@@ -0,0 +1,77 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class Base
+        include Gitlab::Routing.url_helpers
+
+        def initialize(resource = nil)
+          @resource = resource
+        end
+
+        def display_errors
+          message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
+
+          ephemeral_response(text: message)
+        end
+
+        private
+
+        def header_with_list(header, items)
+          message = [header]
+
+          items.each do |item|
+            message << "- #{item}"
+          end
+
+          message.join("\n")
+        end
+
+        def ephemeral_response(message)
+          response = {
+            response_type: :ephemeral,
+            status: 200
+          }.merge(message)
+
+          format_response(response)
+        end
+
+        def in_channel_response(message)
+          response = {
+            response_type: :in_channel,
+            status: 200
+          }.merge(message)
+
+          format_response(response)
+        end
+
+        def format_response(response)
+          response[:text] = format(response[:text]) if response.has_key?(:text)
+
+          if response.has_key?(:attachments)
+            response[:attachments].each do |attachment|
+              attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
+              attachment[:text] = format(attachment[:text]) if attachment[:text]
+            end
+          end
+
+          response
+        end
+
+        # Convert Markdown to slacks format
+        def format(string)
+          Slack::Notifier::LinkFormatter.format(string)
+        end
+
+        def resource_url
+          url_for(
+            [
+              @resource.project.namespace.becomes(Namespace),
+              @resource.project,
+              @resource
+            ]
+          )
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..863d0bf99ca2fc9f3216d3c2a668b9efeda41031
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/deploy.rb
@@ -0,0 +1,21 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class Deploy < Presenters::Base
+        def present(from, to)
+          message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
+
+          in_channel_response(text: message)
+        end
+
+        def no_actions
+          ephemeral_response(text: "No action found to be executed")
+        end
+
+        def too_many_actions
+          ephemeral_response(text: "Too many actions defined")
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cd47b7f4c6ab2fa4559f2868e3abd7a7d16c0039
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/help.rb
@@ -0,0 +1,27 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class Help < Presenters::Base
+        def present(trigger, text)
+          ephemeral_response(text: help_message(trigger, text))
+        end
+
+        private
+
+        def help_message(trigger, text)
+          return "No commands available :thinking_face:" unless @resource.present?
+
+          if text.start_with?('help')
+            header_with_list("Available commands", full_commands(trigger))
+          else
+            header_with_list("Unknown command, these commands are available", full_commands(trigger)) 
+          end
+        end
+
+        def full_commands(trigger)
+          @resource.map { |command| "#{trigger} #{command.help_message}" }
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dfb1c8f6616daec6d3071eabd511a04820208caf
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issuable.rb
@@ -0,0 +1,43 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      module Issuable
+        def color(issuable)
+          issuable.open? ? '#38ae67' : '#d22852'
+        end
+
+        def status_text(issuable)
+          issuable.open? ? 'Open' : 'Closed'
+        end
+
+        def project
+          @resource.project
+        end
+
+        def author
+          @resource.author
+        end
+
+        def fields
+          [
+            {
+              title: "Assignee",
+              value: @resource.assignee ? @resource.assignee.name : "_None_",
+              short: true
+            },
+            {
+              title: "Milestone",
+              value: @resource.milestone ? @resource.milestone.title : "_None_",
+              short: true
+            },
+            {
+              title: "Labels",
+              value: @resource.labels.any? ? @resource.label_names : "_None_",
+              short: true
+            }
+          ]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1a3add56c9ed1d675df2388d0cc1c19514e8190
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -0,0 +1,50 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class IssueNew < Presenters::Base
+        include Presenters::Issuable
+
+        def present
+          in_channel_response(new_issue)
+        end
+
+        private
+
+        def new_issue 
+          {
+            attachments: [
+              {
+                title:        "#{@resource.title} · #{@resource.to_reference}",
+                title_link:   resource_url,
+                author_name:  author.name,
+                author_icon:  author.avatar_url,
+                fallback:     "New issue #{@resource.to_reference}: #{@resource.title}",
+                pretext:      pretext,
+                color:        color(@resource),
+                fields:       fields,
+                mrkdwn_in: [
+                  :title,
+                  :pretext,
+                  :text,
+                  :fields
+                ]
+              }
+            ]
+          }
+        end
+
+        def pretext
+          "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
+        end
+
+        def project_link
+          "[#{project.name_with_namespace}](#{projects_url(project)})"
+        end
+
+        def author_profile_link
+          "[#{author.to_reference}](#{url_for(author)})"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3478359b91d07c595b493bf0a8a9d53dcf659626
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_search.rb
@@ -0,0 +1,47 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class IssueSearch < Presenters::Base
+        include Presenters::Issuable
+
+        def present
+          text = if @resource.count >= 5
+                   "Here are the first 5 issues I found:"
+                 elsif @resource.one?
+                   "Here is the only issue I found:"
+                 else
+                   "Here are the #{@resource.count} issues I found:"
+                 end
+
+          ephemeral_response(text: text, attachments: attachments)
+        end
+
+        private
+
+        def attachments
+          @resource.map do |issue|
+            url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
+
+            {
+              color: color(issue),
+              fallback: "#{issue.to_reference} #{issue.title}",
+              text: "#{url} · #{issue.title} (#{status_text(issue)})",
+
+              mrkdwn_in: [
+                :text
+              ]
+            }
+          end
+        end
+
+        def project
+          @project ||= @resource.first.project
+        end
+
+        def namespace
+          @namespace ||= project.namespace.becomes(Namespace)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe5847ccd157ce59e3fbdf175a1ec2d798e8bd8d
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_show.rb
@@ -0,0 +1,61 @@
+module Gitlab
+  module ChatCommands
+    module Presenters
+      class IssueShow < Presenters::Base
+        include Presenters::Issuable
+
+        def present
+          if @resource.confidential?
+            ephemeral_response(show_issue)
+          else
+            in_channel_response(show_issue)
+          end
+        end
+
+        private
+
+        def show_issue
+          {
+            attachments: [
+              {
+                title:        "#{@resource.title} · #{@resource.to_reference}",
+                title_link:   resource_url,
+                author_name:  author.name,
+                author_icon:  author.avatar_url,
+                fallback:     "Issue #{@resource.to_reference}: #{@resource.title}",
+                pretext:      pretext,
+                text:         text,
+                color:        color(@resource),
+                fields:       fields,
+                mrkdwn_in: [
+                  :pretext,
+                  :text,
+                  :fields
+                ]
+              }
+            ]
+          }
+        end
+
+        def text
+          message = "**#{status_text(@resource)}**"
+
+          if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+            return message
+          end
+
+          message << " · "
+          message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
+          message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
+          message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+
+          message
+        end
+
+        def pretext
+          "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index 1e81eaef18cf553326cd3582c9ec446d22f251e4..b6e924d67bebcd504942564b0889a6db1f0969fe 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::ChatCommands::Command, service: true do
 
       it 'displays the help message' do
         expect(subject[:response_type]).to be(:ephemeral)
-        expect(subject[:text]).to start_with('Available commands')
+        expect(subject[:text]).to start_with('Unknown command')
         expect(subject[:text]).to match('/gitlab issue show')
       end
     end
@@ -34,47 +34,7 @@ describe Gitlab::ChatCommands::Command, service: true do
 
       it 'rejects the actions' do
         expect(subject[:response_type]).to be(:ephemeral)
-        expect(subject[:text]).to start_with('Whoops! That action is not allowed')
-      end
-    end
-
-    context 'issue is successfully created' do
-      let(:params) { { text: "issue create my new issue" } }
-
-      before do
-        project.team << [user, :master]
-      end
-
-      it 'presents the issue' do
-        expect(subject[:text]).to match("my new issue")
-      end
-
-      it 'shows a link to the new issue' do
-        expect(subject[:text]).to match(/\/issues\/\d+/)
-      end
-    end
-
-    context 'searching for an issue' do
-      let(:params) { { text: 'issue search find me' } }
-      let!(:issue) { create(:issue, project: project, title: 'find me') }
-
-      before do
-        project.team << [user, :master]
-      end
-
-      context 'a single issue is found' do
-        it 'presents the issue' do
-          expect(subject[:text]).to match(issue.title)
-        end
-      end
-
-      context 'multiple issues found' do
-        let!(:issue2) { create(:issue, project: project, title: "someone find me") }
-
-        it 'shows a link to the new issue' do
-          expect(subject[:text]).to match(issue.title)
-          expect(subject[:text]).to match(issue2.title)
-        end
+        expect(subject[:text]).to start_with('Whoops! This action is not allowed')
       end
     end
 
@@ -90,7 +50,7 @@ describe Gitlab::ChatCommands::Command, service: true do
       context 'and user can not create deployment' do
         it 'returns action' do
           expect(subject[:response_type]).to be(:ephemeral)
-          expect(subject[:text]).to start_with('Whoops! That action is not allowed')
+          expect(subject[:text]).to start_with('Whoops! This action is not allowed')
         end
       end
 
@@ -100,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do
         end
 
         it 'returns action' do
-          expect(subject[:text]).to include('Deployment from staging to production started.')
+          expect(subject[:text]).to include('Deployment started from staging to production')
           expect(subject[:response_type]).to be(:in_channel)
         end
 
@@ -130,7 +90,7 @@ describe Gitlab::ChatCommands::Command, service: true do
     context 'IssueCreate is triggered' do
       let(:params) { { text: 'issue create my title' } }
 
-      it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
+      it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) }
     end
 
     context 'IssueSearch is triggered' do
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
index bd8099c92da997c1e6a1d2a39b53a83155e9f0a4..b3358a321618d5aa22f3709dd87be5a9bc6f788b 100644
--- a/spec/lib/gitlab/chat_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
     end
 
     context 'if no environment is defined' do
-      it 'returns nil' do
-        expect(subject).to be_nil
+      it 'does not execute an action' do
+        expect(subject[:response_type]).to be(:ephemeral)
+        expect(subject[:text]).to eq("No action found to be executed")
       end
     end
 
@@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
       let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
 
       context 'without actions' do
-        it 'returns nil' do
-          expect(subject).to be_nil
+        it 'does not execute an action' do
+          expect(subject[:response_type]).to be(:ephemeral)
+          expect(subject[:text]).to eq("No action found to be executed")
         end
       end
 
@@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
         end
 
         it 'returns success result' do
-          expect(subject.type).to eq(:success)
-          expect(subject.message).to include('Deployment from staging to production started')
+          expect(subject[:response_type]).to be(:in_channel)
+          expect(subject[:text]).to start_with('Deployment started from staging to production')
         end
 
         context 'when duplicate action exists' do
@@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
           end
 
           it 'returns error' do
-            expect(subject.type).to eq(:error)
-            expect(subject.message).to include('Too many actions defined')
+            expect(subject[:response_type]).to be(:ephemeral)
+            expect(subject[:text]).to eq('Too many actions defined')
           end
         end
 
@@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
                    name: 'teardown', environment: 'production')
           end
 
-          it 'returns success result' do
-            expect(subject.type).to eq(:success)
-            expect(subject.message).to include('Deployment from staging to production started')
+          it 'returns the success message' do
+            expect(subject[:response_type]).to be(:in_channel)
+            expect(subject[:text]).to start_with('Deployment started from staging to production')
           end
         end
       end
diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
similarity index 78%
rename from spec/lib/gitlab/chat_commands/issue_create_spec.rb
rename to spec/lib/gitlab/chat_commands/issue_new_spec.rb
index 6c71e79ff6d163fb12fdd96e0ec095f3cb6939d0..84c22328064e7f6390ba5e6361e315aa34c3f3bc 100644
--- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Gitlab::ChatCommands::IssueCreate, service: true do
+describe Gitlab::ChatCommands::IssueNew, service: true do
   describe '#execute' do
     let(:project) { create(:empty_project) }
     let(:user) { create(:user) }
@@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
       it 'creates the issue' do
         expect { subject }.to change { project.issues.count }.by(1)
 
-        expect(subject.title).to eq('bird is the word')
+        expect(subject[:response_type]).to be(:in_channel)
       end
     end
 
@@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
         expect { subject }.to change { project.issues.count }.by(1)
       end
     end
+
+    context 'issue cannot be created' do
+      let!(:issue)  { create(:issue, project: project, title: 'bird is the word') }
+      let(:regex_match) { described_class.match("issue create #{'a' * 512}}") }
+
+      it 'displays the errors' do
+        expect(subject[:response_type]).to be(:ephemeral)
+        expect(subject[:text]).to match("- Title is too long")
+      end
+    end
   end
 
   describe '.match' do
diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
index 24c06a967fa4ed8f30fe13fca196e283eaced5f7..551ccb79a5866f13176dc95d335511445123e002 100644
--- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
 
 describe Gitlab::ChatCommands::IssueSearch, service: true do
   describe '#execute' do
-    let!(:issue) { create(:issue, title: 'find me') }
+    let!(:issue) { create(:issue, project: project, title: 'find me') }
     let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
-    let(:project) { issue.project }
+    let(:project) { create(:empty_project) }
     let(:user) { issue.author }
     let(:regex_match) { described_class.match("issue search find") }
 
@@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
 
     context 'when the user has no access' do
       it 'only returns the open issues' do
-        expect(subject).not_to include(confidential)
+        expect(subject[:response_type]).to be(:ephemeral)
+        expect(subject[:text]).to match("not found")
       end
     end
 
@@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
       end
 
       it 'returns all results' do
-        expect(subject).to include(confidential, issue)
+        expect(subject).to have_key(:attachments)
+        expect(subject[:text]).to eq("Here are the 2 issues I found:")
       end
     end
 
     context 'without hits on the query' do
       it 'returns an empty collection' do
-        expect(subject).to be_empty
+        expect(subject[:text]).to match("not found")
       end
     end
   end
diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
index 2eab73e49e5deb8224795d1472e5b3f3661c56b1..1f20d0a44ce56eb9c1d5af108c15b8a2c7a7b46e 100644
--- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
 
 describe Gitlab::ChatCommands::IssueShow, service: true do
   describe '#execute' do
-    let(:issue) { create(:issue) }
-    let(:project) { issue.project }
+    let(:issue) { create(:issue, project: project) }
+    let(:project) { create(:empty_project) }
     let(:user) { issue.author }
     let(:regex_match) { described_class.match("issue show #{issue.iid}") }
 
@@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
     end
 
     context 'the issue exists' do
+      let(:title) { subject[:attachments].first[:title] }
+
       it 'returns the issue' do
-        expect(subject.iid).to be issue.iid
+        expect(subject[:response_type]).to be(:in_channel)
+        expect(title).to start_with(issue.title)
       end
 
       context 'when its reference is given' do
         let(:regex_match) { described_class.match("issue show #{issue.to_reference}") }
 
         it 'shows the issue' do
-          expect(subject.iid).to be issue.iid
+          expect(subject[:response_type]).to be(:in_channel)
+          expect(title).to start_with(issue.title)
         end
       end
     end
@@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
     context 'the issue does not exist' do
       let(:regex_match) { described_class.match("issue show 2343242") }
 
-      it "returns nil" do
-        expect(subject).to be_nil
+      it "returns not found" do
+        expect(subject[:response_type]).to be(:ephemeral)
+        expect(subject[:text]).to match("not found")
       end
     end
   end
 
-  describe 'self.match' do
+  describe '.match' do
     it 'matches the iid' do
       match = described_class.match("issue show 123")
 
       expect(match[:iid]).to eq("123")
     end
+
+    it 'accepts a reference' do
+      match = described_class.match("issue show #{Issue.reference_prefix}123")
+
+      expect(match[:iid]).to eq("123")
+    end
   end
 end
diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae41d75ab0c2654df66e2f946d7a43ad0c6bf5f9
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Access do
+  describe '#access_denied' do
+    subject { described_class.new.access_denied }
+
+    it { is_expected.to be_a(Hash) }
+
+    it 'displays an error message' do
+      expect(subject[:text]).to match("is not allowed")
+      expect(subject[:response_type]).to be(:ephemeral)
+    end
+  end
+
+  describe '#not_found' do
+    subject { described_class.new.not_found }
+
+    it { is_expected.to be_a(Hash) }
+
+    it 'tells the user the resource was not found' do
+      expect(subject[:text]).to match("not found!")
+      expect(subject[:response_type]).to be(:ephemeral)
+    end
+  end
+
+  describe '#authorize' do
+    context 'with an authorization URL' do
+      subject { described_class.new('http://authorize.me').authorize }
+
+      it { is_expected.to be_a(Hash) }
+
+      it 'tells the user to authorize' do
+        expect(subject[:text]).to match("connect your GitLab account")
+        expect(subject[:response_type]).to be(:ephemeral)
+      end
+    end
+
+    context 'without authorization url' do
+      subject { described_class.new.authorize }
+
+      it { is_expected.to be_a(Hash) }
+
+      it 'tells the user to authorize' do
+        expect(subject[:text]).to match("Couldn't identify you")
+        expect(subject[:response_type]).to be(:ephemeral)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dc2dd300072f5bd5c63275ad4658e1e7356900c4
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Deploy do
+  let(:build) { create(:ci_build) }
+
+  describe '#present' do
+    subject { described_class.new(build).present('staging', 'prod') }
+
+    it { is_expected.to have_key(:text) }
+    it { is_expected.to have_key(:response_type) }
+    it { is_expected.to have_key(:status) }
+    it { is_expected.not_to have_key(:attachments) }
+
+    it 'messages the channel of the deploy' do
+      expect(subject[:response_type]).to be(:in_channel)
+      expect(subject[:text]).to start_with("Deployment started from staging to prod")
+    end
+  end
+
+  describe '#no_actions' do
+    subject { described_class.new(nil).no_actions }
+
+    it { is_expected.to have_key(:text) }
+    it { is_expected.to have_key(:response_type) }
+    it { is_expected.to have_key(:status) }
+    it { is_expected.not_to have_key(:attachments) }
+
+    it 'tells the user there is no action' do
+      expect(subject[:response_type]).to be(:ephemeral)
+      expect(subject[:text]).to eq("No action found to be executed")
+    end
+  end
+
+  describe '#too_many_actions' do
+    subject { described_class.new([]).too_many_actions }
+
+    it { is_expected.to have_key(:text) }
+    it { is_expected.to have_key(:response_type) }
+    it { is_expected.to have_key(:status) }
+    it { is_expected.not_to have_key(:attachments) }
+
+    it 'tells the user there is no action' do
+      expect(subject[:response_type]).to be(:ephemeral)
+      expect(subject[:text]).to eq("Too many actions defined")
+    end
+  end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..17fcdbc2452c69700d310fe1c7dbf86acfacdf4a
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueNew do
+  let(:project) { create(:empty_project) }
+  let(:issue) { create(:issue, project: project) }
+  let(:attachment) { subject[:attachments].first }
+
+  subject { described_class.new(issue).present }
+
+  it { is_expected.to be_a(Hash) }
+
+  it 'shows the issue' do
+    expect(subject[:response_type]).to be(:in_channel)
+    expect(subject).to have_key(:attachments)
+    expect(attachment[:title]).to start_with(issue.title)
+  end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ec6d3e34a9696df433a497c43b51f17efc2bb2f2
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueSearch do
+  let(:project) { create(:empty_project) }
+  let(:message) { subject[:text] }
+
+  before { create_list(:issue, 2, project: project) }
+
+  subject { described_class.new(project.issues).present }
+
+  it 'formats the message correct' do
+    is_expected.to have_key(:text)
+    is_expected.to have_key(:status)
+    is_expected.to have_key(:response_type)
+    is_expected.to have_key(:attachments)
+  end
+
+  it 'shows a list of results' do
+    expect(subject[:response_type]).to be(:ephemeral)
+
+    expect(message).to start_with("Here are the 2 issues I found")
+  end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5b678d31fce40d90918ea703b140771a266e9e60
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueShow do
+  let(:project) { create(:empty_project) }
+  let(:issue) { create(:issue, project: project) }
+  let(:attachment) { subject[:attachments].first }
+
+  subject { described_class.new(issue).present }
+
+  it { is_expected.to be_a(Hash) }
+
+  it 'shows the issue' do
+    expect(subject[:response_type]).to be(:in_channel)
+    expect(subject).to have_key(:attachments)
+    expect(attachment[:title]).to start_with(issue.title)
+  end
+
+  context 'with upvotes' do
+    before do
+      create(:award_emoji, :upvote, awardable: issue)
+    end
+
+    it 'shows the upvote count' do
+      expect(subject[:response_type]).to be(:in_channel)
+      expect(attachment[:text]).to start_with("**Open** · :+1: 1")
+    end
+  end
+
+  context 'confidential issue' do
+    let(:issue) { create(:issue, project: project) }
+
+    it 'shows an ephemeral response' do
+      expect(subject[:response_type]).to be(:in_channel)
+      expect(attachment[:text]).to start_with("**Open**")
+    end
+  end
+end