Bolt Polling Application

Learn about Slack’s Bolt framework and make a small React application using it.

In this lesson, we build a polling application that allows channel members to participate in a poll.

Create a global shortcut

Our polling application will use a global shortcut. Let’s set up this shortcut by following the steps below:

  1. On the Slack API homepage, ensure your Slack application is selected, and then go to the “Interactivity & Shortcuts” tab on the left-hand sidebar.
  2. Click the “Create New Shortcut” button.
  3. Select the “Global” option and click “Next.”
  4. Name the shortcut “Create a Poll,” add a relevant description, and enter new_poll as the callback ID.
  5. Click “Create” to create the shortcut, then click “Save Changes.”
  6. Refresh the page, and you will be prompted to reinstall the application. Click “reinstall your app” on the yellow message banner.
  7. Click “Allow” to reinstall the application to the workspace.

Application workflow

Any member in the conversation can create a poll using the global shortcut we just created. Interacting with the shortcut opens up a modal form. Upon submitting the form, a message with the poll is sent to the conversation, where users can vote on the poll and add new options.

The slides below show us how the Bolt application will function.

Polling application

The widget below includes the Bolt polling application alongside a React application communicating with the Bolt application and displaying the details of the polls created through it.

Click the “Run” button to start serving the application, then click the output URL at the end of the widget to open up the React application in a new tab. Then, go to Slack and use the “Create a Poll” shortcut in the conversation where the application is added.

Note: The application can only send polls where it is added, so make sure to select only a channel where it is already added!

const { App } = require("@slack/bolt");
const express = require("express");

// Declaring a global variable to store poll data
let allPolls = {};

// An express server to serve requests from the React app
const expressServer = express();
expressServer.use(function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  );
  next();
});

expressServer.get("/", function(req, res) {
  res.send("Hello World");
})

expressServer.get("/polls", function (req, res) {
  res.send(allPolls);
})

const app = new App({
  token: "{{TOKEN}}",
  signingSecret: "{{SIGNING_SECRET}}",
  appToken: "{{APP_TOKEN}}",
  socketMode: true,
});

// Function to add new poll options to a created poll
function addPollOption(newOption, blocks) {
  // Each option takes up three blocks (section, context, divider) 
  const newPollOption = {
    type: "section",
    text: {
      type: "mrkdwn",
      text: `*${newOption}*`,
    },
    accessory: {
      type: "button",
      text: {
        type: "plain_text",
        text: "Vote",
      },
      action_id: "add_vote",
    },
  };

  blocks.splice(blocks.length - 2, 0, newPollOption);

  blocks.splice(blocks.length - 2, 0, {
    type: "context",
    elements: [
      {
        type: "mrkdwn",
        text: "*Vote count: 0*\n",
      },
    ],
  });

  blocks.splice(blocks.length - 2, 0, {
    type: "divider",
  });

  return blocks;
}

// Function to count votes and list voters
function addVote(votedBlockId, blocks, user) {
  let contextBlock;
  for (var i = 0; i < blocks.length; i++) {
    if (blocks[i]["block_id"] === votedBlockId) {
      contextBlock = i + 1;
    }
  }

  let blockText = blocks[contextBlock]["elements"][0]["text"].split(" ");
  if (!blockText.includes(`<@${user}>`)) {
    let newString;
    let originalText = blocks[contextBlock]["elements"][0]["text"];
    originalText = originalText.substring(originalText.indexOf("*", 2) + 1);

    // Extracting the count of votes
    let originalCount = parseInt(blocks[contextBlock]["elements"][0]["text"].substring(13,blocks[contextBlock]["elements"][0]["text"].indexOf("*\n")));

    // Adding one vote to the count and appending the username to the block text
    if (originalCount === 0) {
      newString = `*Vote count: 1*\n <@${user}> `;
    } else {
      newString = `*Vote count: ${
        originalCount + 1
      }*${originalText} <@${user}> `;
    }

    blocks[contextBlock]["elements"][0]["text"] = newString;
  }
  return blocks;
}

// Listener for the new_poll shortcut
app.shortcut("new_poll", async ({ ack, payload, client }) => {
  // Acknowledge shortcut request
  await ack();

  try {
    // Call the views.open method using the WebClient passed to listeners
    const result = await client.views.open({
      trigger_id: payload.trigger_id,
      view: {
        title: {
          type: "plain_text",
          text: "Create a Poll!",
        },
        submit: {
          type: "plain_text",
          text: "Create",
        },
        blocks: [
          {
            type: "input",
            block_id: "poll_title",
            element: {
              type: "plain_text_input",
              action_id: "title",
              placeholder: {
                type: "plain_text",
                text: "Add a title for your poll",
              },
            },
            label: {
              type: "plain_text",
              text: "Title",
            },
            hint: {
              type: "plain_text",
              text: "Example: Should we go for pizza today?",
            },
          },
          {
            type: "input",
            block_id: "first_option",
            element: {
              type: "plain_text_input",
              action_id: "option_1",
              placeholder: {
                type: "plain_text",
                text: "First option",
              },
            },
            label: {
              type: "plain_text",
              text: "Option 1",
            },
          },
          {
            type: "input",
            block_id: "second_option",
            element: {
              type: "plain_text_input",
              action_id: "option_2",
              placeholder: {
                type: "plain_text",
                text: "Second option",
              },
            },
            label: {
              type: "plain_text",
              text: "Option 2",
            },
          },
          {
            block_id: "channel_id",
            type: "input",
            optional: true,
            label: {
              type: "plain_text",
              text: "Select a channel to post the result on",
            },
            element: {
              action_id: "select_conversation",
              type: "conversations_select",
              response_url_enabled: true,
              default_to_current_conversation: true,
              filter: {
                include: ["public"],
                exclude_bot_users: true,
                exclude_external_shared_channels: true
              },
            },
          },
        ],
        type: "modal",
        callback_id: "new_poll",
      },
    });

    console.log(result);
  } catch (error) {
    console.error(error);
  }
});

// Handle a view_submission request
app.view("new_poll", async ({ ack, body, view, client, logger }) => {
  // Acknowledge the view_submission request
  await ack();

  const title = view["state"]["values"]["poll_title"]["title"]["value"];
  const option_1 = view["state"]["values"]["first_option"]["option_1"]["value"];
  const option_2 = view["state"]["values"]["second_option"]["option_2"]["value"];
  const conversation = view["state"]["values"]["channel_id"]["select_conversation"]["selected_conversation"];
  const user = body.user.id;
  const username = body.user.username;

  console.log(body);

  // Message to send user
  let msg = [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `<@${user}> started a poll: *${title}*`,
      },
    },
    {
      type: "divider",
    },
    {
      type: "input",
      block_id: "suggested_option",
      element: {
        type: "plain_text_input",
        action_id: "input",
        placeholder: {
          type: "plain_text",
          text: "Add another option to the poll",
        },
      },
      label: {
        type: "plain_text",
        text: "Suggest an option",
      },
    },
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: {
            type: "plain_text",
            emoji: true,
            text: "Add Option",
          },
          style: "primary",
          action_id: "suggested",
        },
      ],
    },
  ];

  msg = addPollOption(option_1, msg);
  msg = addPollOption(option_2, msg);

  try {
    const response = await client.chat.postMessage({
      channel: conversation,
      blocks: msg,
      text: `<@${user}> started a poll.`,
    });

    const response2 = await client.conversations.info({
      channel: conversation
    })

    allPolls[response['ts']] = {
      "blocks": msg,
      "user" : username,
      "channel": response2['channel']['name']
    };
  } catch (error) {
    logger.error(error);
  }
});

// Handling the suggested action
app.action("suggested", async ({ body, ack, client }) => {
  await ack();
  const ts = body["container"]["message_ts"];
  const channel = body["container"]["channel_id"];
  const user = body["user"]["id"];
  const suggestion = body["state"]["values"]["suggested_option"]["input"]["value"];

  let blocks = body["message"]["blocks"];
  blocks = addPollOption(suggestion, blocks);

  await client.chat.update({
    channel: channel,
    ts: ts,
    blocks: blocks,
    text: `<@${user}> added an option to the poll.`,
  });

  allPolls[ts]['blocks'] = blocks;
});

// Handling the add_vote action
app.action("add_vote", async ({ body, ack, client }) => {
  await ack();

  const ts = body["container"]["message_ts"];
  const channel = body["container"]["channel_id"];

  const blockId = body["actions"][0]["block_id"];
  const user = body["user"]["id"];

  let blocks = body["message"]["blocks"];

  blocks = addVote(blockId, blocks, user);

  await client.chat.update({
    channel: channel,
    ts: ts,
    blocks: blocks,
    text: `<@${user}> voted on the poll.`,
  });

  allPolls[ts]['blocks'] = blocks;
});

// Starting the Bolt app
(async () => {
  await app.start();
  console.log("⚡️ Bolt app is running!");
})();

// Starting the express server
expressServer.listen(3000);
console.log("Server started at http://localhost:3000");
The React/Bolt polling application

Code explanation

The widget above includes both the files for the Bolt app and the React app. Let’s dive a little deeper into the code contained in these files.

The Bolt app

The functions in the bolt-app/app.js file handle these aspects:

  • Lines 8–24: We define an Express server and its routes to serve the requests made by the React application. The poll data stored within the allPolls variable is sent as the response.
  • Line 34: The addPollOption function constructs three blocks (a section block for the value and “Vote” button, a context block for the voting results, and a divider block) for each new option and inserts them in the message blocks.
  • Line 72: The addVote function adds a new vote to an option by checking whether the user has already voted on the option.
  • Line 104: The app.shortcut handler handles the “Create a poll” shortcut by opening up a new modal view.
  • Line 207: The app.view handler handles the submission of the modal view, extracting the relevant information from the view and constructing a message to send to the conversation.
  • Line 290: The app.action handler handles the suggested action when a user adds a new suggestion. It calls the addPollOption function defined on line 34 to add a new option to the message, then updates the original message using the chat.update endpoint.
  • Line 311: The app.action handler handles the add_vote action when a user votes on an option. It makes a call to the addVote function defined on line 72 to add a new vote to the poll.

The React app

The React application contains a single page that displays a list of all polls created through the Bolt application.

The Home component within the react-app/components/Home.js file communicates with the Bolt application by sending requests to the Express server.

  • Line 11: We make a request to the Express server to fetch a list of polls that have been created.
  • Line 30: Once the data has been fetched, we format it accordingly, extracting the required information from the server response.